feat(extraction): add ArkTS language support with ArkUI dispatch bridges (#396, #512, #890 via #648) (#1186)

Adds ArkTS (.ets, HarmonyOS/OpenHarmony) as a first-class language:
full TypeScript-grade extraction via the harmony-contrib tree-sitter
grammar (MIT, vendored byte-identical from the tree-sitter-arkts 0.2.0
npm tarball), plus the ArkUI constructs that make HarmonyOS apps
traceable:

- @Component/@ComponentV2 structs with decorators from both grammar
  positions; members extract as class members with qualified names.
- build() component trees: child instantiation edges via
  arkui_component_expression, no synthesizer needed.
- Attribute chains emitted dot-prefixed and resolved ONLY against
  @Extend/@Styles/@AnimatableExtend/@Builder helpers (unique-or-drop) —
  bare-name fallthrough produced 36,840 wrong edges (17% of calls) on
  the OpenHarmony samples monorepo. All four grammar chain shapes
  handled, including the detached-chain forms.
- .onClick(this.handler) method-reference bindings.
- ohpm workspace modules: bare imports follow oh-package.json5 file:
  deps (ambiguous names dropped), honoring each module's main entry —
  which also lets .ts consumers resolve .ets modules.
- ArkUI dynamic-dispatch bridges, all provenance:'heuristic' with
  wiring-site metadata: assignment-gated state->build() re-render
  (V1 @State family + V2 @Local/@Provider/@Consumer),
  @ohos.events.emitter emit->subscriber pairing on static event keys
  (numeric ids same-file, named constants same-module, fan-out capped),
  and router.pushUrl literal urls -> the target page's @Entry struct.
- $r/$rawfile resource intrinsics treated as built-ins; arkts joins the
  web language family, value-reference edges, re-export chase, and the
  other TS-applicable gates.

Also ships a language-agnostic index-completeness guard: indexAll
stamps index_state (indexing -> complete/partial/failed), reconciles
discovered vs accounted files (a loaded run silently dropped 37 files),
and codegraph status surfaces truncated/partial indexes in human and
--json output.

Validated on HarmoneyOpenEye (82 files), CoolMallArkTS (528, modular
ohpm + ArkUI V2), and openharmony/applications_app_samples (11,693
files, 202,890 nodes stable across re-index, attribute false-positive
audit 36,840 -> 588 residual all-plausible). Supersedes PRs #656 and
#988 with credit — both informed this implementation.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-06 09:07:15 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent f8cdbe3c67
commit 99152212a9
21 changed files with 1695 additions and 21 deletions
+58
View File
@@ -87,3 +87,61 @@ describe('codegraph status --json — CI fields (#329)', () => {
expect(ms).toBeLessThanOrEqual(after + 1000);
});
});
describe('index completeness marker (index_state)', () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-index-state-'));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it('a clean full index stamps state=complete with reconciled counts', async () => {
fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export function f(): number { return 1; }\n');
fs.writeFileSync(path.join(tempDir, 'b.ts'), 'import { f } from "./a";\nexport const y = f();\n');
const cg = CodeGraph.initSync(tempDir);
const result = await cg.indexAll();
// The scan's ground truth is reported and fully accounted for.
expect(result.filesDiscovered).toBeDefined();
expect(result.filesIndexed + result.filesSkipped + result.filesErrored).toBe(
result.filesDiscovered
);
expect(result.errors.filter((e) => e.code === 'index_partial')).toHaveLength(0);
expect(cg.getIndexState()).toBe('complete');
cg.close();
const out = runStatusJson(tempDir);
expect((out.index as Record<string, unknown>).state).toBe('complete');
});
it('a run killed mid-index leaves state=indexing, and status --json surfaces it', async () => {
fs.writeFileSync(path.join(tempDir, 'a.ts'), 'export const x = 1;\n');
const cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.close();
// Simulate a kill between the start-marker write and completion: the
// marker a dead process leaves behind is exactly 'indexing'. Written
// straight into the DB — the process that died can't have cleaned it up.
// (require, not import: vite tries to bundle a dynamic import specifier.)
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(path.join(tempDir, '.codegraph', 'codegraph.db'));
db.prepare(
"INSERT INTO project_metadata (key, value, updated_at) VALUES ('index_state', 'indexing', 0) " +
"ON CONFLICT(key) DO UPDATE SET value = 'indexing'"
).run();
db.close();
const out = runStatusJson(tempDir);
expect((out.index as Record<string, unknown>).state).toBe('indexing');
const reopened = await CodeGraph.open(tempDir);
expect(reopened.getIndexState()).toBe('indexing');
reopened.close();
});
});