import hashlib import json import os from pathlib import Path def load_json_file(path: str, default): try: return json.loads(Path(path).read_text(encoding="utf-8")) except Exception: return default() if callable(default) else default def save_json_file(path: str, payload): Path(path).write_text( json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8", ) class IncrementalAnalysisCache: def __init__(self, cache_path: str, version: int): self.cache_path = cache_path self.version = version self._state = None self._dirty = False self._memo = {} def _default_state(self) -> dict: return { "version": self.version, "files": {}, } def load_state(self) -> dict: if self._state is not None: return self._state payload = load_json_file(self.cache_path, self._default_state) if payload.get("version") != self.version or not isinstance(payload.get("files"), dict): payload = self._default_state() self._state = payload return self._state def save_if_dirty(self): if not self._dirty: return try: save_json_file(self.cache_path, self.load_state()) self._dirty = False except Exception: pass @staticmethod def build_cache_key(analysis_kind: str, path: str) -> str: return f"{analysis_kind}:{os.path.realpath(path)}" @staticmethod def _read_text_with_hash(path: str): try: text = Path(path).read_text(encoding="utf-8", errors="ignore") except Exception: return None, None return text, hashlib.sha256(text.encode("utf-8")).hexdigest() def get_or_compute(self, path: str, analysis_kind: str, analyzer): cache_key = self.build_cache_key(analysis_kind, path) memo_key = (analysis_kind, os.path.realpath(path)) if memo_key in self._memo: return self._memo[memo_key] cache_state = self.load_state() files_cache = cache_state.setdefault("files", {}) cached_entry = files_cache.get(cache_key) try: stat = os.stat(path) stat_signature = { "mtime_ns": stat.st_mtime_ns, "size": stat.st_size, } except Exception: stat_signature = { "mtime_ns": None, "size": None, } if ( isinstance(cached_entry, dict) and cached_entry.get("mtime_ns") == stat_signature["mtime_ns"] and cached_entry.get("size") == stat_signature["size"] and isinstance(cached_entry.get("analysis"), dict) ): analysis = cached_entry["analysis"] self._memo[memo_key] = analysis return analysis text, text_hash = self._read_text_with_hash(path) if text is None: analysis = analyzer("", path) self._memo[memo_key] = analysis return analysis if ( isinstance(cached_entry, dict) and cached_entry.get("sha256") == text_hash and isinstance(cached_entry.get("analysis"), dict) ): cached_entry.update(stat_signature) self._dirty = True analysis = cached_entry["analysis"] self._memo[memo_key] = analysis return analysis analysis = analyzer(text, path) files_cache[cache_key] = { "path": os.path.realpath(path), "analysis_kind": analysis_kind, "mtime_ns": stat_signature["mtime_ns"], "size": stat_signature["size"], "sha256": text_hash, "analysis": analysis, } self._dirty = True self._memo[memo_key] = analysis return analysis