Просмотр исходного кода

feat(extraction): extract function-valued properties of exported-const objects

`export const actions = { default: async () => {...} }` (SvelteKit form actions,
and general JS handler/route/reducer maps) left the arrow functions unextracted —
the walker skips object-literal functions (deliberately, to avoid inline-object
noise like `ctx.set({...})`). So an action's body (and its calls) was invisible.

Now: for an EXPORTED const whose initializer is an object literal, extract each
function-valued property (arrow / function expression) as a function named by its
key and walk its body. extractFunction gains a nameOverride so ONLY this explicit
path names pair-arrows — inline-object arrows reached by the general walker still
fall through to the <anonymous> skip, so no noise returns. JS/TS-gated.

Validated: fixtures extract the actions + walk bodies (default→helper, default→
api.post resolve); SvelteKit detection doesn't break it. Blast radius tiny:
excalidraw +1 node, Python (django) +0, Vue repos +0, realworld +11 (the actions).

Known residual: a `$lib`-alias namespace-member call (`api.post`) from an extracted
action node doesn't resolve even though the same alias resolves for `load` — a
deeper resolver interaction, separate from this extraction change. Local/relative
calls from actions connect fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry 1 месяц назад
Родитель
Сommit
5c4f98c4d3
1 измененных файлов с 26 добавлено и 2 удалено
  1. 26 2
      src/extraction/tree-sitter.ts

+ 26 - 2
src/extraction/tree-sitter.ts

@@ -516,7 +516,7 @@ export class TreeSitterExtractor {
   /**
    * Extract a function
    */
-  private extractFunction(node: SyntaxNode): void {
+  private extractFunction(node: SyntaxNode, nameOverride?: string): void {
     if (!this.extractor) return;
 
     // If the language provides getReceiverType and this function has a receiver
@@ -526,12 +526,17 @@ export class TreeSitterExtractor {
       return;
     }
 
-    let name = extractName(node, this.source, this.extractor);
+    // nameOverride is supplied only for explicitly-named anonymous functions the
+    // caller resolved itself (e.g. arrow values of exported-const object members
+    // — SvelteKit actions). Inline-object arrows reached by the general walker
+    // get no override, so they still fall through to the <anonymous> skip below.
+    let name = nameOverride ?? extractName(node, this.source, this.extractor);
     // For arrow functions and function expressions assigned to variables,
     // resolve the name from the parent variable_declarator.
     // e.g. `export const useAuth = () => { ... }` — the arrow_function node
     // has no `name` field; the name lives on the variable_declarator.
     if (
+      !nameOverride &&
       name === '<anonymous>' &&
       (node.type === 'arrow_function' || node.type === 'function_expression')
     ) {
@@ -1057,6 +1062,25 @@ export class TreeSitterExtractor {
             if (varNode) {
               this.extractVariableTypeAnnotation(child, varNode.id);
             }
+
+            // Exported const object-of-functions: `export const actions =
+            // { default: async () => {} }` (SvelteKit form actions / handler maps
+            // / route tables). Extract each function-valued property as a function
+            // named by its key + walk its body so its calls (e.g. api.post) are
+            // captured. Scoped to EXPORTED consts to exclude the inline-object
+            // noise (`ctx.set({...})`) the object-method skip deliberately avoids.
+            if (isExported && valueNode &&
+                (valueNode.type === 'object' || valueNode.type === 'object_expression')) {
+              for (let j = 0; j < valueNode.namedChildCount; j++) {
+                const pair = valueNode.namedChild(j);
+                if (pair?.type !== 'pair') continue;
+                const v = getChildByField(pair, 'value');
+                const k = getChildByField(pair, 'key');
+                if (k && v && (v.type === 'arrow_function' || v.type === 'function_expression')) {
+                  this.extractFunction(v, getNodeText(k, this.source).replace(/^['"`]|['"`]$/g, ''));
+                }
+              }
+            }
           }
         }
       }