Преглед изворни кода

fix(extraction): index TS/JS generator function declarations and expressions (#1741) (#1743)

Tree-sitter kinds generator_function_declaration / generator_function were
missing from both the wasm and native kernel function-type lists, so
function* / async function* (and const g = function* () {}) produced no
nodes. Add the kinds on both paths and cover TS+JS declaration/expression
forms in extraction tests.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Colby Mchenry пре 8 часа
родитељ
комит
df435d50d1

+ 57 - 0
__tests__/extraction.test.ts

@@ -745,6 +745,63 @@ export const fetchData = async () => {
   });
   });
 });
 });
 
 
+describe('Generator Function Extraction (#1741)', () => {
+  const functionNames = (file: string, code: string) =>
+    extractFromSource(file, code)
+      .nodes.filter((n) => n.kind === 'function')
+      .map((n) => n.name)
+      .sort();
+
+  it('extracts function* and async function* declarations in TypeScript', () => {
+    process.env.CODEGRAPH_KERNEL = '0';
+    const code = `
+function plain() { return 1; }
+function* gen() { yield 2; }
+async function asyncFn() { return 3; }
+async function* asyncGen() { yield 4; }
+`;
+    expect(functionNames('gens.ts', code)).toEqual(['asyncFn', 'asyncGen', 'gen', 'plain']);
+  });
+
+  it('extracts function* and async function* declarations in JavaScript', () => {
+    process.env.CODEGRAPH_KERNEL = '0';
+    const code = `
+function plain() { return 1; }
+function* gen() { yield 2; }
+async function asyncFn() { return 3; }
+async function* asyncGen() { yield 4; }
+`;
+    expect(functionNames('gens.js', code)).toEqual(['asyncFn', 'asyncGen', 'gen', 'plain']);
+  });
+
+  it('extracts const-assigned generator and async generator expressions (TS)', () => {
+    process.env.CODEGRAPH_KERNEL = '0';
+    const code = `
+const g = function* () { yield 1; };
+const ag = async function* () { yield 2; };
+export const exportedGen = function* () { yield 3; };
+`;
+    const result = extractFromSource('gen-expr.ts', code);
+    const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name).sort();
+    expect(names).toEqual(['ag', 'exportedGen', 'g']);
+    expect(result.nodes.find((n) => n.name === 'exportedGen')?.isExported).toBe(true);
+    expect(result.nodes.find((n) => n.name === 'g')?.isExported).toBeFalsy();
+  });
+
+  it('extracts const-assigned generator and async generator expressions (JS)', () => {
+    process.env.CODEGRAPH_KERNEL = '0';
+    const code = `
+const g = function* () { yield 1; };
+const ag = async function* () { yield 2; };
+export const exportedGen = function* () { yield 3; };
+`;
+    const result = extractFromSource('gen-expr.js', code);
+    const names = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name).sort();
+    expect(names).toEqual(['ag', 'exportedGen', 'g']);
+    expect(result.nodes.find((n) => n.name === 'exportedGen')?.isExported).toBe(true);
+  });
+});
+
 describe('Type Alias Extraction', () => {
 describe('Type Alias Extraction', () => {
   it('should extract exported type aliases in TypeScript', () => {
   it('should extract exported type aliases in TypeScript', () => {
     const code = `
     const code = `

+ 3 - 3
codegraph-kernel/src/tsjs/extractors.rs

@@ -23,7 +23,7 @@ impl<'t> Walker<'t> {
         // variable_declarator (`export const useAuth = () => {}`).
         // variable_declarator (`export const useAuth = () => {}`).
         if name_override.is_none()
         if name_override.is_none()
             && name == "<anonymous>"
             && name == "<anonymous>"
-            && matches!(node.kind(), "arrow_function" | "function_expression")
+            && matches!(node.kind(), "arrow_function" | "function_expression" | "generator_function")
         {
         {
             if let Some(parent) = node.parent() {
             if let Some(parent) = node.parent() {
                 if parent.kind() == "variable_declarator" {
                 if parent.kind() == "variable_declarator" {
@@ -342,9 +342,9 @@ impl<'t> Walker<'t> {
             }
             }
             let name = self.text(name_node).to_string();
             let name = self.text(name_node).to_string();
 
 
-            // Arrow/function values extract as functions, named by the declarator.
+            // Arrow/function/generator values extract as functions, named by the declarator.
             if let Some(v) = value {
             if let Some(v) = value {
-                if matches!(v.kind(), "arrow_function" | "function_expression") {
+                if matches!(v.kind(), "arrow_function" | "function_expression" | "generator_function") {
                     self.extract_function(v, None);
                     self.extract_function(v, None);
                     continue;
                     continue;
                 }
                 }

+ 2 - 2
codegraph-kernel/src/tsjs/mod.rs

@@ -60,7 +60,7 @@ fn is_method_type(v: Variant, kind: &str) -> bool {
 }
 }
 
 
 fn is_function_type(kind: &str) -> bool {
 fn is_function_type(kind: &str) -> bool {
-    matches!(kind, "function_declaration" | "arrow_function" | "function_expression")
+    matches!(kind, "function_declaration" | "generator_function_declaration" | "arrow_function" | "function_expression" | "generator_function")
 }
 }
 
 
 fn is_class_type(v: Variant, kind: &str) -> bool {
 fn is_class_type(v: Variant, kind: &str) -> bool {
@@ -792,7 +792,7 @@ impl<'t> Walker<'t> {
         if let Some(name_node) = node.child_by_field_name("name") {
         if let Some(name_node) = node.child_by_field_name("name") {
             return self.text(name_node).to_string();
             return self.text(name_node).to_string();
         }
         }
-        if matches!(node.kind(), "arrow_function" | "function_expression") {
+        if matches!(node.kind(), "arrow_function" | "function_expression" | "generator_function") {
             return "<anonymous>".to_string();
             return "<anonymous>".to_string();
         }
         }
         for i in 0..node.named_child_count() {
         for i in 0..node.named_child_count() {

+ 1 - 1
src/extraction/languages/javascript.ts

@@ -3,7 +3,7 @@ import type { LanguageExtractor } from '../tree-sitter-types';
 import { classifyTsClassMember } from './typescript';
 import { classifyTsClassMember } from './typescript';
 
 
 export const javascriptExtractor: LanguageExtractor = {
 export const javascriptExtractor: LanguageExtractor = {
-  functionTypes: ['function_declaration', 'arrow_function', 'function_expression'],
+  functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
   classTypes: ['class_declaration'],
   classTypes: ['class_declaration'],
   methodTypes: ['method_definition', 'field_definition'],
   methodTypes: ['method_definition', 'field_definition'],
   // JS `field_definition` ≙ TS `public_field_definition`: plain fields are
   // JS `field_definition` ≙ TS `public_field_definition`: plain fields are

+ 1 - 1
src/extraction/languages/typescript.ts

@@ -39,7 +39,7 @@ export function classifyTsClassMember(node: SyntaxNode): 'method' | 'property' {
 }
 }
 
 
 export const typescriptExtractor: LanguageExtractor = {
 export const typescriptExtractor: LanguageExtractor = {
-  functionTypes: ['function_declaration', 'arrow_function', 'function_expression'],
+  functionTypes: ['function_declaration', 'generator_function_declaration', 'arrow_function', 'function_expression', 'generator_function'],
   classTypes: ['class_declaration', 'abstract_class_declaration'],
   classTypes: ['class_declaration', 'abstract_class_declaration'],
   methodTypes: ['method_definition', 'public_field_definition'],
   methodTypes: ['method_definition', 'public_field_definition'],
   classifyMethodNode: classifyTsClassMember,
   classifyMethodNode: classifyTsClassMember,

+ 3 - 3
src/extraction/tree-sitter.ts

@@ -171,7 +171,7 @@ function extractNameRaw(node: SyntaxNode, source: string, extractor: LanguageExt
   // not from identifiers in their body. Without this, single-expression arrow
   // not from identifiers in their body. Without this, single-expression arrow
   // functions like `const fn = () => someIdentifier` get named "someIdentifier"
   // functions like `const fn = () => someIdentifier` get named "someIdentifier"
   // instead of "fn", because the fallback below finds the body identifier.
   // instead of "fn", because the fallback below finds the body identifier.
-  if (node.type === 'arrow_function' || node.type === 'function_expression') {
+  if (node.type === 'arrow_function' || node.type === 'function_expression' || node.type === 'generator_function') {
     return '<anonymous>';
     return '<anonymous>';
   }
   }
 
 
@@ -1556,7 +1556,7 @@ export class TreeSitterExtractor {
     if (
     if (
       !nameOverride &&
       !nameOverride &&
       name === '<anonymous>' &&
       name === '<anonymous>' &&
-      (node.type === 'arrow_function' || node.type === 'function_expression')
+      (node.type === 'arrow_function' || node.type === 'function_expression' || node.type === 'generator_function')
     ) {
     ) {
       const parent = node.parent;
       const parent = node.parent;
       if (parent?.type === 'variable_declarator') {
       if (parent?.type === 'variable_declarator') {
@@ -2623,7 +2623,7 @@ export class TreeSitterExtractor {
             }
             }
             const name = getNodeText(nameNode, this.source);
             const name = getNodeText(nameNode, this.source);
             // Arrow functions / function expressions: extract as function instead of variable
             // Arrow functions / function expressions: extract as function instead of variable
-            if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
+            if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression' || valueNode.type === 'generator_function')) {
               this.extractFunction(valueNode);
               this.extractFunction(valueNode);
               continue;
               continue;
             }
             }