129 lines
3.8 KiB
Python
129 lines
3.8 KiB
Python
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
|