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:
co-authored by
Claude Fable 5
parent
f8cdbe3c67
commit
99152212a9
@@ -0,0 +1,128 @@
|
||||
import type { LanguageExtractor } from '../tree-sitter-types';
|
||||
import { typescriptExtractor } from './typescript';
|
||||
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||
|
||||
/**
|
||||
* ArkTS (HarmonyOS / OpenHarmony, `.ets`) — a TypeScript superset whose
|
||||
* headline feature is declarative UI: an `@Component struct` with a `build()`
|
||||
* method describing the view tree, `@State`/`@Prop`/`@Link` reactive
|
||||
* properties, and global `@Builder`/`@Extend`/`@Styles` functions.
|
||||
*
|
||||
* The vendored grammar (harmony-contrib/tree-sitter-arkts) extends
|
||||
* tree-sitter-javascript exactly the way tree-sitter-typescript does, so every
|
||||
* TS node type — and therefore the whole typescriptExtractor — applies
|
||||
* verbatim. ArkTS-specific shapes it adds:
|
||||
*
|
||||
* - `struct_declaration` / `struct_body` — the `@Component struct`. Same
|
||||
* `name:`/`body:` fields as class_declaration; members are ordinary
|
||||
* `method_definition` / `public_field_definition` nodes, so struct members
|
||||
* extract through the standard class-member paths.
|
||||
* - `arkui_component_expression` — a build()-DSL component instantiation
|
||||
* (`Column() { … }`). Carries a `function:` field (the component), an
|
||||
* optional `children:` block, and — unlike TS — the CHAINED ATTRIBUTES as
|
||||
* repeated `property:`/`arguments:` field pairs on the SAME node
|
||||
* (`Text(x).fontSize(16).opacity(0.6)` is ONE node, not nested calls).
|
||||
* Handled by the arkts branch in extractCall (tree-sitter.ts).
|
||||
* - Decorators on functions (`@Builder function F() {}`) — invalid in TS,
|
||||
* first-class here (a `decorator:` field on function_declaration), so the
|
||||
* core's existing extractDecoratorsFor path captures them.
|
||||
*/
|
||||
|
||||
/** Reactive/state decorators that make a member worth flagging (searchable). */
|
||||
const DECORATED_MEMBER_TYPES = new Set([
|
||||
'struct_declaration',
|
||||
'public_field_definition',
|
||||
'method_definition',
|
||||
'function_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Collect decorator names for a declaration from BOTH positions the grammar
|
||||
* produces: direct `decorator` children (`@Entry @Component struct X`,
|
||||
* `@State count` on a field) and preceding `decorator` siblings (`@Builder`
|
||||
* before a method_definition inside struct_body; `@Component` on the
|
||||
* export_statement wrapping `export struct X`). The backwards sibling walk
|
||||
* stops at the first non-decorator so an earlier declaration's decorators
|
||||
* never leak in (mirrors extractDecoratorsFor's sibling pass).
|
||||
*/
|
||||
function collectDecoratorNames(node: SyntaxNode): string[] | undefined {
|
||||
const names: string[] = [];
|
||||
const nameOf = (dec: SyntaxNode): string | undefined => {
|
||||
for (let i = 0; i < dec.namedChildCount; i++) {
|
||||
const child = dec.namedChild(i);
|
||||
if (!child) continue;
|
||||
if (child.type === 'identifier') return child.text;
|
||||
if (child.type === 'call_expression') {
|
||||
// `@StorageLink('theme')` / `@Extend(Text)` — the decorator name is
|
||||
// the callee.
|
||||
const fn = child.childForFieldName('function');
|
||||
if (fn?.type === 'identifier') return fn.text;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type === 'decorator') {
|
||||
const n = nameOf(child);
|
||||
if (n) names.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
const parent = node.parent;
|
||||
if (parent) {
|
||||
// Find this node among the parent's named children by start offset
|
||||
// (wrapper identity is not stable across navigation), then walk backwards.
|
||||
const start = node.startIndex;
|
||||
let idx = -1;
|
||||
for (let i = 0; i < parent.namedChildCount; i++) {
|
||||
const sib = parent.namedChild(i);
|
||||
if (sib && sib.startIndex === start) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = idx - 1; i >= 0; i--) {
|
||||
const sib = parent.namedChild(i);
|
||||
if (!sib || sib.type !== 'decorator') break;
|
||||
const n = nameOf(sib);
|
||||
if (n) names.unshift(n);
|
||||
}
|
||||
}
|
||||
|
||||
return names.length > 0 ? names : undefined;
|
||||
}
|
||||
|
||||
export const arktsExtractor: LanguageExtractor = {
|
||||
...typescriptExtractor,
|
||||
|
||||
// `@Component struct X { … }` — extractStruct handles it (kind `struct`,
|
||||
// members extracted like class members, `this.m()` resolution and the
|
||||
// class/struct containment gates in the name-matcher all apply as-is). The
|
||||
// component-ness is preserved on the node's decorators (`Component`,
|
||||
// `Entry`, `CustomDialog`, `Reusable`), captured by extractModifiers below.
|
||||
structTypes: ['struct_declaration'],
|
||||
|
||||
// build()-DSL component instantiations are call sites: `TodoRow({...})`
|
||||
// inside a parent's build() is the parent→child component edge, resolved by
|
||||
// the ordinary call pipeline against the child's struct node. The arkts
|
||||
// branch in extractCall also lifts each chained `.attr(...)` (emitted
|
||||
// dot-prefixed so it can ONLY resolve to `@Extend`/`@Styles`/`@Builder`
|
||||
// attribute helpers — see matchReference) and `.onXxx(this.handler)`
|
||||
// method-reference bindings. `leading_dot_expression` is the detached-chain
|
||||
// shape the grammar produces when a nested component's chain starts on the
|
||||
// line after its closing `}` inside arkui_children.
|
||||
callTypes: ['call_expression', 'arkui_component_expression', 'leading_dot_expression'],
|
||||
|
||||
// Surface ArkTS decorators on the node's `decorators` list (searchable, and
|
||||
// the hook a future ArkUI state→build synthesizer keys off). Core paths
|
||||
// already emit `decorates` REFERENCES for classes/methods/properties/
|
||||
// functions; this hook is what puts the names on struct nodes too —
|
||||
// extractStruct has no extractDecoratorsFor call, and node.decorators is
|
||||
// only populated via extractModifiers (see createNode).
|
||||
extractModifiers: (node) => {
|
||||
if (!DECORATED_MEMBER_TYPES.has(node.type)) return undefined;
|
||||
return collectDecoratorNames(node);
|
||||
},
|
||||
};
|
||||
@@ -34,6 +34,7 @@ import { vbnetExtractor } from './vbnet';
|
||||
import { erlangExtractor } from './erlang';
|
||||
import { solidityExtractor } from './solidity';
|
||||
import { terraformExtractor } from './terraform';
|
||||
import { arktsExtractor } from './arkts';
|
||||
|
||||
export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
|
||||
typescript: typescriptExtractor,
|
||||
@@ -65,4 +66,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
|
||||
erlang: erlangExtractor,
|
||||
solidity: solidityExtractor,
|
||||
terraform: terraformExtractor,
|
||||
arkts: arktsExtractor,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user