From 9146eab76a2533803c4d4f58f72aa116f1f2d8a5 Mon Sep 17 00:00:00 2001 From: Demands Date: Sun, 5 Jul 2026 21:56:45 +0300 Subject: [PATCH 1/3] feat: register enforcescript language token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds enforcescript (DayZ modding language, .c) as a language token that reuses the vendored C# grammar. Built-in EXTENSION_MAP is untouched (.c stays c by default) — enforcescript only activates via a project's codegraph.json extensions override. --- src/extraction/grammars.ts | 4 +++- src/types.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index aef4cac22..85b2ddc38 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -47,6 +47,7 @@ const WASM_GRAMMAR_FILES: Record = { erlang: 'tree-sitter-erlang.wasm', solidity: 'tree-sitter-solidity.wasm', terraform: 'tree-sitter-terraform.wasm', + enforcescript: 'tree-sitter-c_sharp.wasm', }; /** @@ -292,7 +293,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise Date: Sun, 5 Jul 2026 21:58:00 +0300 Subject: [PATCH 2/3] feat: add enforcescript extractor with C# grammar reuse preParse rewrites EnforceScript-only syntax (foreach colon form, void ~destructor, modded class inheritance, extends, proto/event/notnull/inout/autoptr, #ifdef) into shapes the C# grammar parses cleanly. --- src/extraction/languages/enforcescript.ts | 157 ++++++++++++++++++++++ src/extraction/languages/index.ts | 2 + 2 files changed, 159 insertions(+) create mode 100644 src/extraction/languages/enforcescript.ts diff --git a/src/extraction/languages/enforcescript.ts b/src/extraction/languages/enforcescript.ts new file mode 100644 index 000000000..4b0332296 --- /dev/null +++ b/src/extraction/languages/enforcescript.ts @@ -0,0 +1,157 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import { getNodeText } from '../tree-sitter-helpers'; +import type { ExtractorContext, LanguageExtractor } from '../tree-sitter-types'; +import { csharpExtractor } from './csharp'; + +/** + * Enforce Script (DayZ modding language, `.c`) reuses the vendored C# grammar + * — there's no dedicated tree-sitter grammar for it, and its OOP syntax is + * close enough to C# that a handful of textual rewrites ahead of parsing + * (below) let the C# grammar parse it cleanly. All rewrites preserve line + * count (never touch newlines) and, except where noted, preserve exact byte + * offsets too, so node positions stay accurate. + */ + +/** Modded classes: `modded` is rewritten to this real C# modifier keyword + * (same length, so offsets don't shift) so the class still parses as a class + * — `sealed` doesn't exist in Enforce Script, so no real code collides with it. */ +const MODDED_SENTINEL = 'sealed'; + +/** Marks an unresolved reference as the modded→original namesake link so the + * dedicated framework resolver (not general name-matching, which would risk + * a self-loop or an arbitrary pick among same-named files) is the only thing + * that ever claims it. Exported so the resolver can match on the exact prefix. */ +export const MODDED_LINK_PREFIX = '__enforcescript_modded__:'; + +/** + * Blank C-style conditional-compilation directives. Enforce Script uses + * `#ifdef`/`#ifndef` (real C#'s preprocessor only has `#if`/`#elif`/`#else`/ + * `#endif` — no `-def`/`-ndef` forms), which otherwise doesn't parse as a + * directive at all and corrupts the rest of the file's top-level structure. + * Blanking (not converting to `#if`) matches the existing C# extractor's + * policy of indexing every symbol regardless of build flags. + */ +function blankPreprocessorDirectives(source: string): string { + if (source.indexOf('#') === -1) return source; + const re = /^([ \t]*)#[ \t]*(ifdef|ifndef|if|elif|else|endif|define|undef)\b[^\n]*/gm; + return source.replace(re, (m, indent: string) => indent + ' '.repeat(m.length - indent.length)); +} + +/** + * `modded class Foo` / `modded class Foo: Bar` / `modded class Foo extends + * Bar`. The compiler silently ignores any inheritance clause on a modded + * class — `Foo` stays a descendant of its own original, never `Bar` + * (modded-classes.md #2) — so keeping the clause would create a false + * `extends` edge. Rewritten to a bare `sealed class Foo` (no bases at all); + * `extractModifiers`/`synthesizeMembers` below use the sentinel to flag the + * node and link it to its namesake instead. + */ +function markModdedAndStripInheritance(source: string): string { + source = source.replace(/\bmodded\b(?=\s+class\b)/g, MODDED_SENTINEL); + const re = new RegExp(`\\b${MODDED_SENTINEL}\\s+class\\s+\\w+\\s*(:\\s*\\w+|extends\\s+\\w+)`, 'g'); + return source.replace(re, (m, clause: string) => m.slice(0, m.length - clause.length) + clause.replace(/[^\n]/g, ' ')); +} + +/** + * `class Foo extends Bar` — Java-style inheritance real C# doesn't parse at + * all (an ERROR node that detaches the base but leaves the body intact). + * Rewritten to colon form, which the grammar already handles natively. + */ +function convertExtendsToColon(source: string): string { + return source.replace(/\bclass(\s+\w+)\s+extends\b/g, (_m, nameGroup: string) => `class${nameGroup} :` + ' '.repeat('extends'.length - 1)); +} + +/** + * Keyword modifiers with no C# equivalent that otherwise cause the grammar to + * misparse ` ` as `type: modifier, name: Type` with + * the real name orphaned in an ERROR sibling (the documented `proto Man + * GetPlayer()` bug, and the same shape for event/notnull/inout/autoptr). + * `proto(\s+native)?` is blanked as one unit — `native` alone after `proto` + * is blanked would still misparse the same way. `ref` and `auto` are + * deliberately NOT included: `ref` is already a valid C# modifier and `auto` + * parses fine structurally as an (unknown) type-position identifier. + */ +function blankUnknownModifiers(source: string): string { + return source.replace(/\bproto(\s+native)?\b|\bevent\b|\bnotnull\b|\binout\b|\bautoptr\b/g, (m) => ' '.repeat(m.length)); +} + +/** + * `void ~ClassName()` — Enforce Script destructors declare an explicit `void` + * return type (memory-refs.md #9); real C# destructors have none (a distinct + * `destructor_declaration` grammar rule). The leftover `void` collides with + * that rule, and on real files the resulting ERROR cascades and corrupts the + * parse of everything after it (confirmed against a 9.7k-line vanilla file). + */ +function blankVoidBeforeDestructor(source: string): string { + return source.replace(/\bvoid\b(?=\s+~\s*\w+\s*\()/g, (m) => ' '.repeat(m.length)); +} + +/** + * `foreach (Type v: coll)` / `foreach (Type k, Type v: map)` + * (language-rules.md #5) — C#'s foreach uses `in`, not `:`. Same cascading- + * failure shape as the destructor case: foreach appears constantly in real + * code, so one unhandled instance corrupts the rest of the file. NOT byte- + * offset preserving (the standard style has no space before the colon, so + * there's no room for ` in `) — shifts columns for the rest of that one line + * only; no newlines are touched, so every other line stays exact. + */ +function convertForeachColonToIn(source: string): string { + return source.replace(/\bforeach(\s*)\(([^)]*)\)/g, (m, ws: string, inner: string) => { + const colonIdx = inner.indexOf(':'); + if (colonIdx === -1) return m; + const before = inner.slice(0, colonIdx).replace(/\s+$/, ''); + const after = inner.slice(colonIdx + 1).replace(/^\s+/, ''); + return `foreach${ws}(${before} in ${after})`; + }); +} + +function preprocessEnforceScript(source: string): string { + source = blankPreprocessorDirectives(source); + source = markModdedAndStripInheritance(source); + source = convertExtendsToColon(source); + source = blankUnknownModifiers(source); + source = blankVoidBeforeDestructor(source); + source = convertForeachColonToIn(source); + return source; +} + +/** A class_declaration carries the `modded` sentinel modifier iff the source declared it `modded`. */ +function hasModdedSentinel(node: SyntaxNode): boolean { + for (let i = 0; i < node.childCount; i++) { + const child = node.child(i); + if (child?.type === 'modifier' && child.text === MODDED_SENTINEL) return true; + } + return false; +} + +export const enforcescriptExtractor: LanguageExtractor = { + ...csharpExtractor, + preParse: preprocessEnforceScript, + // `void ~ClassName()` parses as a first-class `destructor_declaration` once + // blankVoidBeforeDestructor removes the leading `void` — same name/params/ + // body field shape as method_declaration, so no extra hooks are needed. + methodTypes: [...csharpExtractor.methodTypes, 'destructor_declaration'], + extractModifiers: (node) => (hasModdedSentinel(node) ? ['modded'] : undefined), + // `modded class Foo` has no bases (the inheritance clause was stripped + // pre-parse), so no false `extends` edge is ever created. This links it to + // its namesake instead — the sentinel-prefixed reference name ensures only + // the dedicated framework resolver (frameworks/enforcescript-modded.ts) + // ever resolves it, never general name-matching (which has no self- + // exclusion and would risk a self-loop when this is the only same-named + // symbol in the project). + synthesizeMembers: (classNode, ctx: ExtractorContext) => { + if (!hasModdedSentinel(classNode)) return; + const nameNode = classNode.childForFieldName('name'); + const classId = ctx.nodeStack[ctx.nodeStack.length - 1]; + if (!nameNode || !classId) return; + ctx.addUnresolvedReference({ + fromNodeId: classId, + referenceName: MODDED_LINK_PREFIX + getNodeText(nameNode, ctx.source), + referenceKind: 'references', + line: classNode.startPosition.row + 1, + column: classNode.startPosition.column, + filePath: ctx.filePath, + language: 'enforcescript', + }); + }, +}; \ No newline at end of file diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index ab8c8aa07..facf10167 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -34,6 +34,7 @@ import { vbnetExtractor } from './vbnet'; import { erlangExtractor } from './erlang'; import { solidityExtractor } from './solidity'; import { terraformExtractor } from './terraform'; +import { enforcescriptExtractor } from './enforcescript'; export const EXTRACTORS: Partial> = { typescript: typescriptExtractor, @@ -65,4 +66,5 @@ export const EXTRACTORS: Partial> = { erlang: erlangExtractor, solidity: solidityExtractor, terraform: terraformExtractor, + enforcescript: enforcescriptExtractor, }; From 068abfc1a7b57ce94156f0c76521b3406614d987 Mon Sep 17 00:00:00 2001 From: Demands Date: Sun, 5 Jul 2026 21:58:18 +0300 Subject: [PATCH 3/3] feat: link modded class Foo to its original declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit modded class Foo silently drops any inheritance clause at compile time, so the extractor emits a sentinel-prefixed reference (__enforcescript_modded__:Foo) instead of a real name. Only this dedicated resolver ever claims that prefix — general name-matching has no self-exclusion and would risk a self-loop edge when the modded class is the only same-named symbol in the project. --- .../frameworks/enforcescript-modded.ts | 56 +++++++++++++++++++ src/resolution/frameworks/index.ts | 4 ++ 2 files changed, 60 insertions(+) create mode 100644 src/resolution/frameworks/enforcescript-modded.ts diff --git a/src/resolution/frameworks/enforcescript-modded.ts b/src/resolution/frameworks/enforcescript-modded.ts new file mode 100644 index 000000000..9490e5b2a --- /dev/null +++ b/src/resolution/frameworks/enforcescript-modded.ts @@ -0,0 +1,56 @@ +/** + * Enforce Script `modded class` → original namesake resolver. + * + * A `modded class Foo` extractor node (src/extraction/languages/enforcescript.ts) + * emits a self-referential reference named `__enforcescript_modded__:Foo` + * instead of a real base-class name, because the compiler drops any + * inheritance clause on a modded class outright (modded-classes.md #2) — `Foo` + * stays a descendant of its OWN original, never whatever followed `:`/ + * `extends`. General name-matching is deliberately never given this ref: it + * has no self-exclusion, and `getNodesByName('Foo')` would include the modded + * node itself, risking a self-loop `references` edge when it's the only `Foo` + * in the project. The sentinel prefix guarantees this resolver is the only + * thing that ever claims it (claimsReference below), so self-exclusion and + * the non-modded/earliest-declared preference can be enforced deliberately. + */ + +import type { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types'; +import { MODDED_LINK_PREFIX } from '../../extraction/languages/enforcescript'; + +export const enforcescriptModdedResolver: FrameworkResolver = { + name: 'enforcescript-modded', + languages: ['enforcescript'], + + detect(context: ResolutionContext): boolean { + return context.getNodesByKind('class').some((n) => n.language === 'enforcescript'); + }, + + claimsReference(name: string): boolean { + return name.startsWith(MODDED_LINK_PREFIX); + }, + + resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + if (!ref.referenceName.startsWith(MODDED_LINK_PREFIX)) return null; + const name = ref.referenceName.slice(MODDED_LINK_PREFIX.length); + + const candidates = context + .getNodesByName(name) + .filter((n) => n.kind === 'class' && n.language === 'enforcescript' && n.id !== ref.fromNodeId); + if (candidates.length === 0) return null; + + // Prefer the true original (not itself modded) — the one every `new Foo()` + // ultimately bottoms out at. Otherwise fall back to the earliest-declared + // modded version as a best-effort stand-in for "the previous link in the + // modded chain" (modded-classes.md #3) — real load order isn't knowable + // statically, so this is an approximation, not a guarantee. + const original = candidates.find((n) => !n.decorators?.includes('modded')); + const chosen = original ?? candidates.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)[0]!; + + return { + original: ref, + targetNodeId: chosen.id, + confidence: 0.75, + resolvedBy: 'framework', + }; + }, +}; \ No newline at end of file diff --git a/src/resolution/frameworks/index.ts b/src/resolution/frameworks/index.ts index 91da9a01c..72f0cc47f 100644 --- a/src/resolution/frameworks/index.ts +++ b/src/resolution/frameworks/index.ts @@ -29,6 +29,7 @@ import { expoModulesResolver } from './expo-modules'; import { fabricViewResolver } from './fabric'; import { cicsResolver } from './cics'; import { terraformResolver } from './terraform'; +import { enforcescriptModdedResolver } from './enforcescript-modded'; /** * All registered framework resolvers @@ -76,6 +77,8 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [ cicsResolver, // Terraform / OpenTofu — disambiguate var/local/module/resource refs to same-dir module terraformResolver, + // Enforce Script — link `modded class Foo` to its original namesake + enforcescriptModdedResolver, ]; /** @@ -152,3 +155,4 @@ export { swiftObjcBridgeResolver } from './swift-objc'; export { reactNativeBridgeResolver } from './react-native'; export { expoModulesResolver } from './expo-modules'; export { fabricViewResolver } from './fabric'; +export { enforcescriptModdedResolver } from './enforcescript-modded';