|
|
@@ -0,0 +1,282 @@
|
|
|
+import type { Node as SyntaxNode } from 'web-tree-sitter';
|
|
|
+import { getNodeText, getChildByField } from '../tree-sitter-helpers';
|
|
|
+import type { LanguageExtractor } from '../tree-sitter-types';
|
|
|
+
|
|
|
+/**
|
|
|
+ * Solidity extractor — tree-sitter-solidity (ABI 14).
|
|
|
+ *
|
|
|
+ * Solidity has multiple top-level "contract-like" containers (contract /
|
|
|
+ * interface / library) and several callable forms that don't have a `name:`
|
|
|
+ * field (constructor, fallback, receive). We map:
|
|
|
+ * - contract_declaration → class (also library_declaration)
|
|
|
+ * - interface_declaration → interface
|
|
|
+ * - struct_declaration → struct
|
|
|
+ * - enum_declaration → enum (enum_value is the bare ident — no
|
|
|
+ * name field, so handled in visitNode)
|
|
|
+ * - function_definition / modifier_definition → function|method
|
|
|
+ * - constructor_definition / fallback_receive_definition → method (synthetic
|
|
|
+ * name: "constructor" / "fallback" / "receive" — these are nameless in AST)
|
|
|
+ * - state_variable_declaration / struct_member → field (inside contract/struct)
|
|
|
+ * - event_definition / error_declaration → field-shaped node carrying
|
|
|
+ * the event/error name so callers/refs can resolve emit X / revert X
|
|
|
+ * - import_directive → import
|
|
|
+ * - call_expression / emit_statement / revert_statement / modifier_invocation
|
|
|
+ * → calls (the latter three are call-shaped but use distinct AST nodes)
|
|
|
+ */
|
|
|
+
|
|
|
+function getInheritanceAncestors(node: SyntaxNode, source: string): string[] {
|
|
|
+ const ancestors: string[] = [];
|
|
|
+ for (let i = 0; i < node.namedChildCount; i++) {
|
|
|
+ const child = node.namedChild(i);
|
|
|
+ if (!child || child.type !== 'inheritance_specifier') continue;
|
|
|
+ const ancestor = getChildByField(child, 'ancestor');
|
|
|
+ if (!ancestor) continue;
|
|
|
+ // ancestor is user_defined_type → contains identifier (or scoped path)
|
|
|
+ const id = ancestor.descendantsOfType('identifier');
|
|
|
+ if (id.length > 0) {
|
|
|
+ const last = id[id.length - 1]!;
|
|
|
+ ancestors.push(getNodeText(last, source));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ancestors;
|
|
|
+}
|
|
|
+
|
|
|
+function fallbackReceiveName(node: SyntaxNode): string {
|
|
|
+ // tree-sitter-solidity reuses one node type for both `fallback() ...` and
|
|
|
+ // `receive() ...` — the keyword is an unnamed/anonymous child. Walk all
|
|
|
+ // children (named + unnamed) and pick the first whose text is one of these.
|
|
|
+ for (let i = 0; i < node.childCount; i++) {
|
|
|
+ const child = node.child(i);
|
|
|
+ if (!child) continue;
|
|
|
+ const t = child.text;
|
|
|
+ if (t === 'fallback' || t === 'receive') return t;
|
|
|
+ }
|
|
|
+ return 'fallback';
|
|
|
+}
|
|
|
+
|
|
|
+export const solidityExtractor: LanguageExtractor = {
|
|
|
+ // Free functions (file-level) AND methods inside contracts use the same
|
|
|
+ // function_definition node — the dispatcher routes by isInsideClassLikeNode.
|
|
|
+ functionTypes: ['function_definition', 'modifier_definition'],
|
|
|
+ classTypes: ['contract_declaration', 'library_declaration'],
|
|
|
+ methodTypes: [
|
|
|
+ 'function_definition',
|
|
|
+ 'modifier_definition',
|
|
|
+ 'constructor_definition',
|
|
|
+ 'fallback_receive_definition',
|
|
|
+ ],
|
|
|
+ interfaceTypes: ['interface_declaration'],
|
|
|
+ structTypes: ['struct_declaration'],
|
|
|
+ enumTypes: ['enum_declaration'],
|
|
|
+ enumMemberTypes: [], // enum_value has no name field; handled in visitNode
|
|
|
+ typeAliasTypes: ['user_defined_type_definition'],
|
|
|
+ importTypes: ['import_directive'],
|
|
|
+ // emit / revert / modifier_invocation are call-shaped but distinct AST nodes
|
|
|
+ callTypes: ['call_expression', 'emit_statement', 'revert_statement', 'modifier_invocation'],
|
|
|
+ // top-level state vars are file-scope constants/variables; struct_member
|
|
|
+ // and state_variable_declaration inside a contract are fields (handled via
|
|
|
+ // fieldTypes + isInsideClassLikeNode).
|
|
|
+ variableTypes: ['state_variable_declaration', 'constant_variable_declaration'],
|
|
|
+ fieldTypes: ['state_variable_declaration', 'struct_member'],
|
|
|
+
|
|
|
+ nameField: 'name',
|
|
|
+ bodyField: 'body',
|
|
|
+ paramsField: 'parameters',
|
|
|
+ returnField: 'return_type',
|
|
|
+
|
|
|
+ // constructor / fallback / receive have no `name:` field — synthesize one.
|
|
|
+ resolveName: (node, _source) => {
|
|
|
+ if (node.type === 'constructor_definition') return 'constructor';
|
|
|
+ if (node.type === 'fallback_receive_definition') return fallbackReceiveName(node);
|
|
|
+ return undefined;
|
|
|
+ },
|
|
|
+
|
|
|
+ getSignature: (node, source) => {
|
|
|
+ // tree-sitter-solidity does NOT wrap params in a `parameters:` field — each
|
|
|
+ // `parameter` node is a direct child of function/modifier/constructor. We
|
|
|
+ // reconstruct `(t1 a, t2 b)` by walking those siblings; getChildByField
|
|
|
+ // would return null and lose the entire param list.
|
|
|
+ const params: string[] = [];
|
|
|
+ let returnType: SyntaxNode | undefined;
|
|
|
+ let visibility: SyntaxNode | undefined;
|
|
|
+ let mutability: SyntaxNode | undefined;
|
|
|
+ for (let i = 0; i < node.namedChildCount; i++) {
|
|
|
+ const child = node.namedChild(i);
|
|
|
+ if (!child) continue;
|
|
|
+ const fieldName = node.fieldNameForNamedChild(i);
|
|
|
+ if (child.type === 'parameter' && fieldName !== 'return_type') {
|
|
|
+ params.push(getNodeText(child, source));
|
|
|
+ } else if (child.type === 'return_type_definition' || fieldName === 'return_type') {
|
|
|
+ returnType = child;
|
|
|
+ } else if (child.type === 'visibility') {
|
|
|
+ visibility = child;
|
|
|
+ } else if (child.type === 'state_mutability') {
|
|
|
+ mutability = child;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const parts: string[] = [];
|
|
|
+ parts.push(`(${params.join(', ')})`);
|
|
|
+ if (visibility) parts.push(getNodeText(visibility, source));
|
|
|
+ if (mutability) parts.push(getNodeText(mutability, source));
|
|
|
+ if (returnType) parts.push(getNodeText(returnType, source));
|
|
|
+ return parts.join(' ');
|
|
|
+ },
|
|
|
+
|
|
|
+ getVisibility: (node) => {
|
|
|
+ // Solidity functions: public/private/internal/external — `external` maps
|
|
|
+ // to 'public' for our purposes (callable from outside the contract).
|
|
|
+ for (let i = 0; i < node.namedChildCount; i++) {
|
|
|
+ const child = node.namedChild(i);
|
|
|
+ if (child?.type !== 'visibility') continue;
|
|
|
+ const t = child.text.trim();
|
|
|
+ if (t === 'public' || t === 'external') return 'public';
|
|
|
+ if (t === 'private') return 'private';
|
|
|
+ if (t === 'internal') return 'internal';
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ },
|
|
|
+
|
|
|
+ // `constant_variable_declaration` is by definition a constant; the generic
|
|
|
+ // variable extractor defaults to kind:'variable' otherwise.
|
|
|
+ isConst: (node) => node.type === 'constant_variable_declaration',
|
|
|
+
|
|
|
+ visitNode: (node, ctx) => {
|
|
|
+ const t = node.type;
|
|
|
+
|
|
|
+ // Solidity inheritance: `contract MyToken is Token, IERC20 { ... }`. The
|
|
|
+ // core's extractInheritance walks for `extends_clause`/`base_class_clause`
|
|
|
+ // shaped children, which Solidity doesn't have — its `inheritance_specifier`
|
|
|
+ // children are direct siblings of the `body:` field. We piggyback on the
|
|
|
+ // standard contract/library/interface dispatch (which fires AFTER this
|
|
|
+ // hook returns false) by emitting the extends references here, then
|
|
|
+ // returning false so the generic class extractor still creates the node.
|
|
|
+ // Each ancestor → one `extends` reference; the resolver then upgrades it
|
|
|
+ // to a real edge. Without these refs, "what inherits from Ownable" /
|
|
|
+ // "trace inherited onlyOwner" can't traverse the contract graph and the
|
|
|
+ // agent has to Read each file to reconstruct the hierarchy.
|
|
|
+ if (
|
|
|
+ t === 'contract_declaration' ||
|
|
|
+ t === 'library_declaration' ||
|
|
|
+ t === 'interface_declaration'
|
|
|
+ ) {
|
|
|
+ // Mirror the generic class path — create the node (the extends refs
|
|
|
+ // need its id), emit the refs, walk the body — then return true to
|
|
|
+ // short-circuit the generic dispatch so nothing is doubled.
|
|
|
+ const ancestors = getInheritanceAncestors(node, ctx.source);
|
|
|
+ const nameNode = getChildByField(node, 'name');
|
|
|
+ const body = getChildByField(node, 'body');
|
|
|
+ if (!nameNode) return false;
|
|
|
+ const name = getNodeText(nameNode, ctx.source);
|
|
|
+ const kind = t === 'interface_declaration' ? 'interface' : 'class';
|
|
|
+ const created = ctx.createNode(kind, name, node);
|
|
|
+ if (!created) return true;
|
|
|
+ // Solidity uses one keyword (`is`) for both class-extends-class and
|
|
|
+ // class-implements-interface, indistinguishable at parse time. Emit
|
|
|
+ // `extends` for every ancestor — the resolver's interface-impl synthesizer
|
|
|
+ // (Phase 5.5) reclassifies a class→interface edge as `implements` based
|
|
|
+ // on the target node kind, matching how Java/C# extractors do it.
|
|
|
+ for (const ancestor of ancestors) {
|
|
|
+ ctx.addUnresolvedReference({
|
|
|
+ fromNodeId: created.id,
|
|
|
+ referenceName: ancestor,
|
|
|
+ referenceKind: 'extends',
|
|
|
+ line: node.startPosition.row + 1,
|
|
|
+ column: node.startPosition.column,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ ctx.pushScope(created.id);
|
|
|
+ if (body) {
|
|
|
+ for (let i = 0; i < body.namedChildCount; i++) {
|
|
|
+ const child = body.namedChild(i);
|
|
|
+ if (child) ctx.visitNode(child);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ctx.popScope();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // tree-sitter-solidity puts struct_member / enum_value as DIRECT children
|
|
|
+ // of struct_declaration / enum_declaration — there is no `body:` field, so
|
|
|
+ // the core's extractStruct/extractEnum (which require a body field) bails.
|
|
|
+ // We extract these here, push the parent on the scope stack, walk the
|
|
|
+ // direct children, and emit one struct/enum node + its members.
|
|
|
+ if (t === 'struct_declaration' || t === 'enum_declaration') {
|
|
|
+ const nameNode = getChildByField(node, 'name');
|
|
|
+ if (!nameNode) return true;
|
|
|
+ const name = getNodeText(nameNode, ctx.source);
|
|
|
+ const kind = t === 'struct_declaration' ? 'struct' : 'enum';
|
|
|
+ const created = ctx.createNode(kind, name, node);
|
|
|
+ if (!created) return true;
|
|
|
+ ctx.pushScope(created.id);
|
|
|
+ for (let i = 0; i < node.namedChildCount; i++) {
|
|
|
+ const child = node.namedChild(i);
|
|
|
+ if (!child) continue;
|
|
|
+ if (child === nameNode) continue;
|
|
|
+ ctx.visitNode(child);
|
|
|
+ }
|
|
|
+ ctx.popScope();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // enum_value is the bare identifier of an enum case — no `name:` field, so
|
|
|
+ // the generic enum-member dispatch can't find it. Use the node's own text.
|
|
|
+ if (t === 'enum_value') {
|
|
|
+ ctx.createNode('enum_member', getNodeText(node, ctx.source), node);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // event SomeEvent(...) — preserve event name as a field-shaped node so
|
|
|
+ // `emit SomeEvent(...)` (an emit_statement) can resolve to it. We use
|
|
|
+ // `field` kind because Solidity events are member declarations of a
|
|
|
+ // contract, similar in spirit to fields, and `field` reuses the FTS index
|
|
|
+ // without adding a new NodeKind.
|
|
|
+ if (t === 'event_definition') {
|
|
|
+ const nameNode = getChildByField(node, 'name');
|
|
|
+ if (!nameNode) return true;
|
|
|
+ const name = getNodeText(nameNode, ctx.source);
|
|
|
+ ctx.createNode('field', name, node, {
|
|
|
+ signature: getNodeText(node, ctx.source).trim().slice(0, 200),
|
|
|
+ });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // error MyError(...) — same reasoning as event_definition. revert MyError()
|
|
|
+ // (a revert_statement) is captured via callTypes and resolves by name.
|
|
|
+ if (t === 'error_declaration') {
|
|
|
+ const nameNode = getChildByField(node, 'name');
|
|
|
+ if (!nameNode) return true;
|
|
|
+ const name = getNodeText(nameNode, ctx.source);
|
|
|
+ ctx.createNode('field', name, node, {
|
|
|
+ signature: getNodeText(node, ctx.source).trim().slice(0, 200),
|
|
|
+ });
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // struct_member: named field inside a struct. It has `name:` + `type:` —
|
|
|
+ // the generic field dispatch handles it via fieldTypes, so no custom code.
|
|
|
+ return false;
|
|
|
+ },
|
|
|
+
|
|
|
+ // import "X"; / import {A, B} from "X"; / import * as X from "Y";
|
|
|
+ // We surface the SOURCE path as the moduleName — that's what
|
|
|
+ // import-resolver matches against on disk. The `import_name:` field (if
|
|
|
+ // present, for the symbolic-import form) is intentionally ignored here; the
|
|
|
+ // SOURCE is the file being imported from.
|
|
|
+ extractImport: (node, source) => {
|
|
|
+ const importText = source.substring(node.startIndex, node.endIndex).trim();
|
|
|
+ const sourceField = getChildByField(node, 'source');
|
|
|
+ if (!sourceField) return null;
|
|
|
+ // source is a `string` node — strip quotes via descendantsOfType lookup.
|
|
|
+ const stringContent = sourceField.descendantsOfType('string_literal');
|
|
|
+ let moduleName: string;
|
|
|
+ if (stringContent.length > 0) {
|
|
|
+ moduleName = getNodeText(stringContent[0]!, source);
|
|
|
+ } else {
|
|
|
+ moduleName = getNodeText(sourceField, source);
|
|
|
+ }
|
|
|
+ moduleName = moduleName.replace(/^["']|["']$/g, '').trim();
|
|
|
+ if (!moduleName) return null;
|
|
|
+ return { moduleName, signature: importText };
|
|
|
+ },
|
|
|
+};
|