Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,11 @@ language.tpl=.ts
viz_node_limit=0
```

The declaration applies everywhere graphify keys a decision on the extension — file classification, extractor dispatch, cross-file resolution, and the AST cache (a file re-parsed under a different language never reuses the old entry). It is read by `graphify extract`, `graphify update`, `watch`, the hooks, the MCP server, and the `/graphify` skill alike. A typo is reported once on stderr and the scan continues with graphify's defaults.
The declaration controls file classification, extractor selection and language-specific grammar and resolution, and the AST cache: a file re-parsed under a different language never reuses the old entry. Original source paths and physical format conventions, such as Fortran `.F90` preprocessing and C/C++ header/implementation pairing, are preserved. Optional language backends must still be installed. A typo is reported once on stderr and the scan continues with graphify's defaults.

Adding, changing, or removing a language declaration invalidates affected incremental manifest entries even when the source bytes have not changed. `graphify watch` recognizes declared source extensions and root `.graphifyrc` changes, including atomic replacement; configuration changes schedule an AST rebuild and flag semantic work for the next extraction. The same configuration is read by `graphify extract`, `graphify update`, the hooks, the MCP server, and the `/graphify` skill.

Extensionless local JS/TS imports can also resolve declared suffixes. Native suffixes retain precedence within file and directory-index lookup, and file candidates are tried before directory indexes. Python module and package lookups also support declared Python suffixes, while retaining native `.py` precedence.

---

Expand Down
8 changes: 4 additions & 4 deletions graphify/csharp_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"""
from __future__ import annotations

_CSHARP_SUFFIXES = (".cs",)
from graphify.extractors.csharp import _is_cs_file

DISPATCH_RELATION = "dispatches_to"

Expand All @@ -44,7 +44,7 @@ def _is_csharp(node: dict | None) -> bool:
if not node:
return False
source_file = node.get("source_file")
return bool(source_file) and str(source_file).endswith(_CSHARP_SUFFIXES)
return bool(source_file) and _is_cs_file(str(source_file))


def _method_label(node: dict) -> str:
Expand Down Expand Up @@ -74,11 +74,11 @@ def resolve_csharp_interface_dispatch(
place, since the call site really does name the interface.
"""
if not any(
str(result.get("source_file", "")).endswith(_CSHARP_SUFFIXES)
_is_csharp(result)
for result in per_file
if isinstance(result, dict)
) and not any(
str(n.get("source_file", "")).endswith(_CSHARP_SUFFIXES) for n in all_nodes
_is_csharp(n) for n in all_nodes
):
return

Expand Down
22 changes: 20 additions & 2 deletions graphify/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
google_workspace_enabled,
)
from graphify.paths import GRAPHIFY_OUT, out_path
from graphify.rcfile import activate_language_overrides, effective_suffix
from graphify.rcfile import activate_language_overrides, cache_salt, effective_suffix


class FileType(str, Enum):
Expand Down Expand Up @@ -2227,6 +2227,9 @@ def save_manifest(
re-queues the file after the failure is fixed, without deleting
graphify-out/.
"""
# Another root may have activated different overrides in this thread.
if root is not None:
activate_language_overrides(Path(root))
existing = load_manifest(manifest_path, root=root)

# Index both raw and NFC forms so scan/clear membership survives the
Expand Down Expand Up @@ -2347,12 +2350,19 @@ def _normalise_entry(entry):
mtime, h = hashed[f]
key = _nfc(f)
prev = _normalise_entry(existing.get(key, {})) or {}
# Hash workers do not inherit this thread's language overrides.
current_lang = cache_salt(f)
lang_changed = current_lang != prev.get("language")
if kind in ("ast", "both"):
ast_h = h
elif lang_changed:
ast_h = ""
else:
ast_h = prev.get("ast_hash", "")
if kind in ("semantic", "both"):
sem_h = h
elif lang_changed:
sem_h = ""
else:
# Preserve semantic_hash only when content is unchanged
sem_h = prev.get("semantic_hash", "") if h == prev.get("ast_hash", "") else ""
Expand All @@ -2365,6 +2375,7 @@ def _normalise_entry(entry):
and mtime == prev.get("mtime")
and (ast_h == prev.get("ast_hash", "") if kind in ("ast", "both") else True)
and (sem_h == prev.get("semantic_hash", "") if kind in ("semantic", "both") else True)
and not lang_changed
and not _in_clear_ast(f)
and not _in_clear(f)
)
Expand All @@ -2374,6 +2385,9 @@ def _normalise_entry(entry):
"ast_hash": ast_h,
"semantic_hash": sem_h,
}
if current_lang is not None:
# No override keeps legacy rows unchanged.
entry["language"] = current_lang
manifest[key] = entry
if root is not None:
# Persist in portable form: forward-slash relative paths. Keys outside
Expand Down Expand Up @@ -2504,7 +2518,11 @@ def detect_incremental(
# the graph drifted from disk (#1859). No stored hash means we
# cannot verify content — any mtime delta forces a re-extract,
# and the next save promotes the entry into the dict schema.
if isinstance(stored, (int, float)):
# A language change invalidates even identical bytes and legacy rows.
stored_lang = stored.get("language") if isinstance(stored, dict) else None
if stored is not None and cache_salt(f) != stored_lang:
changed = True
elif isinstance(stored, (int, float)):
changed = current_mtime != stored
elif isinstance(stored, dict):
# Normalise legacy {mtime, hash} to new schema
Expand Down
Loading
Loading