Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc86ea96dc | ||
|
|
d1adc8331a | ||
|
|
80d363f145 | ||
|
|
12be2025f1 |
@@ -10,6 +10,19 @@ All notable changes to GitNexus will be documented in this file.
|
||||
- Added automatic cleanup of stale KuzuDB index files
|
||||
- LadybugDB v0.15 requires explicit VECTOR extension loading for semantic search
|
||||
|
||||
## [1.5.3] - 2026-04-01
|
||||
|
||||
### Added
|
||||
|
||||
- **TypeScript/JavaScript MethodExtractor config** — shared extraction config covering abstract methods, visibility modifiers, async/override keywords, decorators, rest/optional/destructured parameters, and return types (#588) — @compound-ai
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Azure OpenAI compatibility** — use `max_completion_tokens` instead of deprecated `max_tokens` (newer models reject `max_tokens`); skip `temperature` for Azure provider (some models reject non-default values) (#618)
|
||||
- **Simplified Azure interactive setup** — 3 prompts (endpoint, deployment, key) instead of 7 (#618)
|
||||
- **Wiki HTML viewer script injection** — escape `</script>` in embedded JSON so LLM-generated markdown no longer breaks the viewer (#618)
|
||||
- Ensure import rewrites survive npm publish lifecycle
|
||||
|
||||
## [1.4.0] - 2026-03-13
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitnexus",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
"dependencies": {
|
||||
"@huggingface/transformers": "^3.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gitnexus",
|
||||
"version": "1.5.2",
|
||||
"version": "1.5.3",
|
||||
"description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
|
||||
"author": "Abhigyan Patwari",
|
||||
"license": "PolyForm-Noncommercial-1.0.0",
|
||||
|
||||
+18
-58
@@ -232,65 +232,28 @@ export const wikiCommand = async (inputPath?: string, options?: WikiCommandOptio
|
||||
|
||||
llmConfig = { ...llmConfig, provider: 'cursor', model, apiKey: '', baseUrl: '' };
|
||||
} else if (choice === '3') {
|
||||
// Azure OpenAI guided setup
|
||||
console.log('\n Azure OpenAI setup.');
|
||||
console.log(
|
||||
' You need: your resource name, deployment name, and API key from the Azure portal.\n',
|
||||
);
|
||||
// Azure OpenAI guided setup — minimal prompts
|
||||
console.log('\n Azure OpenAI setup.\n');
|
||||
|
||||
const resourceName = (
|
||||
await prompt(' Azure resource name (e.g. my-openai-resource): ')
|
||||
).trim();
|
||||
if (!resourceName) {
|
||||
console.log('\n No resource name provided. Aborting.\n');
|
||||
const endpoint = (
|
||||
await prompt(' Endpoint URL (e.g. https://my-resource.openai.azure.com): ')
|
||||
)
|
||||
.trim()
|
||||
.replace(/\/+$/, '');
|
||||
if (!endpoint) {
|
||||
console.log('\n No endpoint provided. Aborting.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const deploymentName = (
|
||||
await prompt(' Deployment name (the name you gave your model deployment): ')
|
||||
).trim();
|
||||
const deploymentName = (await prompt(' Deployment name: ')).trim();
|
||||
if (!deploymentName) {
|
||||
console.log('\n No deployment name provided. Aborting.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Offer v1 or legacy URL
|
||||
console.log('\n API format:');
|
||||
console.log(' [1] v1 API — recommended (no api-version needed)');
|
||||
console.log(' [2] Legacy — uses api-version query param\n');
|
||||
const apiFormat = await prompt(' Select format (1/2, default: 1): ');
|
||||
|
||||
let azureApiVersion: string | undefined;
|
||||
let azureBaseUrl: string;
|
||||
if (apiFormat === '2') {
|
||||
const versionInput = await prompt(' api-version (default: 2024-10-21): ');
|
||||
azureApiVersion = versionInput || '2024-10-21';
|
||||
azureBaseUrl = `https://${resourceName}.openai.azure.com/openai/deployments/${deploymentName}`;
|
||||
} else {
|
||||
azureBaseUrl = `https://${resourceName}.openai.azure.com/openai/v1`;
|
||||
azureApiVersion = undefined;
|
||||
}
|
||||
|
||||
defaultModel = deploymentName;
|
||||
|
||||
// Ask if this is a reasoning model deployment
|
||||
const reasoningAnswer = await prompt(
|
||||
' Is this a reasoning model (o1, o3, o4-mini)? (y/N): ',
|
||||
);
|
||||
const isReasoningModelDeployment = ['y', 'yes'].includes(reasoningAnswer.toLowerCase());
|
||||
|
||||
if (isReasoningModelDeployment) {
|
||||
console.log(
|
||||
' Note: temperature and max_tokens will be omitted for this deployment (Azure reasoning model requirement).\n',
|
||||
);
|
||||
}
|
||||
|
||||
const modelInput = await prompt(` Model / deployment name (default: ${defaultModel}): `);
|
||||
const model = modelInput || defaultModel;
|
||||
|
||||
// API key
|
||||
// API key — use env var if available
|
||||
const envKey = process.env.GITNEXUS_API_KEY || process.env.OPENAI_API_KEY || '';
|
||||
let azureKey: string;
|
||||
if (envKey) {
|
||||
@@ -311,26 +274,23 @@ export const wikiCommand = async (inputPath?: string, options?: WikiCommandOptio
|
||||
return;
|
||||
}
|
||||
|
||||
// Save Azure config including optional apiVersion and isReasoningModel
|
||||
const azureConfig: Parameters<typeof saveCLIConfig>[0] = {
|
||||
// Always use v1 API format — no need for api-version
|
||||
const azureBaseUrl = `${endpoint}/openai/v1`;
|
||||
|
||||
await saveCLIConfig({
|
||||
apiKey: azureKey,
|
||||
baseUrl: azureBaseUrl,
|
||||
model,
|
||||
model: deploymentName,
|
||||
provider: 'azure',
|
||||
isReasoningModel: isReasoningModelDeployment,
|
||||
};
|
||||
if (azureApiVersion) azureConfig.apiVersion = azureApiVersion;
|
||||
await saveCLIConfig(azureConfig);
|
||||
});
|
||||
console.log(' Config saved to ~/.gitnexus/config.json\n');
|
||||
|
||||
llmConfig = {
|
||||
...llmConfig,
|
||||
apiKey: azureKey,
|
||||
baseUrl: azureBaseUrl,
|
||||
model,
|
||||
model: deploymentName,
|
||||
provider: 'azure',
|
||||
apiVersion: azureApiVersion,
|
||||
isReasoningModel: isReasoningModelDeployment,
|
||||
};
|
||||
} else {
|
||||
// OpenAI-compatible provider (OpenAI, OpenRouter, Custom)
|
||||
|
||||
@@ -15,12 +15,17 @@ import type { FieldVisibility } from '../../field-types.js';
|
||||
|
||||
/**
|
||||
* Check whether any child of `node` (named or unnamed) has .text matching
|
||||
* one of the given `keywords`.
|
||||
* the given `keyword`.
|
||||
*
|
||||
* Skips the `name` field child to avoid false positives when a method is
|
||||
* named after a contextual keyword (e.g. `abstract()` in TypeScript).
|
||||
*/
|
||||
export function hasKeyword(node: SyntaxNode, keyword: string): boolean {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child && child.text.trim() === keyword) return true;
|
||||
if (!child || child === nameNode) continue;
|
||||
if (child.text.trim() === keyword) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -46,6 +51,7 @@ export function hasModifier(node: SyntaxNode, modifierType: string, keyword: str
|
||||
/**
|
||||
* Return the first matching visibility keyword found either as a direct keyword
|
||||
* child or inside a modifier wrapper node.
|
||||
* Skips the `name` field child (same rationale as hasKeyword).
|
||||
*/
|
||||
export function findVisibility(
|
||||
node: SyntaxNode,
|
||||
@@ -53,10 +59,12 @@ export function findVisibility(
|
||||
defaultVis: FieldVisibility,
|
||||
modifierNodeType?: string,
|
||||
): FieldVisibility {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
// Direct keyword children
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
const text = child?.text.trim() as FieldVisibility | undefined;
|
||||
if (!child || child === nameNode) continue;
|
||||
const text = child.text.trim() as FieldVisibility | undefined;
|
||||
if (text && (keywords as ReadonlySet<string>).has(text)) return text;
|
||||
}
|
||||
// Modifier wrapper
|
||||
|
||||
@@ -17,6 +17,11 @@ import { TYPESCRIPT_QUERIES, JAVASCRIPT_QUERIES } from '../tree-sitter-queries.j
|
||||
import { typescriptFieldExtractor } from '../field-extractors/typescript.js';
|
||||
import { createFieldExtractor } from '../field-extractors/generic.js';
|
||||
import { javascriptConfig } from '../field-extractors/configs/typescript-javascript.js';
|
||||
import { createMethodExtractor } from '../method-extractors/generic.js';
|
||||
import {
|
||||
typescriptMethodConfig,
|
||||
javascriptMethodConfig,
|
||||
} from '../method-extractors/configs/typescript-javascript.js';
|
||||
|
||||
const BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'console',
|
||||
@@ -124,6 +129,7 @@ export const typescriptProvider = defineLanguage({
|
||||
importResolver: resolveTypescriptImport,
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
fieldExtractor: typescriptFieldExtractor,
|
||||
methodExtractor: createMethodExtractor(typescriptMethodConfig),
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
||||
@@ -136,5 +142,6 @@ export const javascriptProvider = defineLanguage({
|
||||
importResolver: resolveJavascriptImport,
|
||||
namedBindingExtractor: extractTsNamedBindings,
|
||||
fieldExtractor: createFieldExtractor(javascriptConfig),
|
||||
methodExtractor: createMethodExtractor(javascriptMethodConfig),
|
||||
builtInNames: BUILT_INS,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
// gitnexus/src/core/ingestion/method-extractors/configs/typescript-javascript.ts
|
||||
// Verified against tree-sitter-typescript ^0.23.2, tree-sitter-javascript ^0.23.0
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import type {
|
||||
MethodExtractionConfig,
|
||||
ParameterInfo,
|
||||
MethodVisibility,
|
||||
} from '../../method-types.js';
|
||||
import { hasKeyword } from '../../field-extractors/configs/helpers.js';
|
||||
import { extractSimpleTypeName } from '../../type-extractors/shared.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TS/JS helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const VISIBILITY_KEYWORDS = new Set<MethodVisibility>(['public', 'private', 'protected']);
|
||||
|
||||
/**
|
||||
* Extract parameters from formal_parameters.
|
||||
*
|
||||
* Handles both TS node types (required_parameter, optional_parameter, rest_parameter)
|
||||
* and JS node types (identifier, assignment_pattern, rest_pattern), plus destructured
|
||||
* parameters (object_pattern, array_pattern) in both grammars.
|
||||
*/
|
||||
function extractTsJsParameters(node: SyntaxNode): ParameterInfo[] {
|
||||
const paramList = node.childForFieldName('parameters');
|
||||
if (!paramList) return [];
|
||||
const params: ParameterInfo[] = [];
|
||||
|
||||
for (let i = 0; i < paramList.namedChildCount; i++) {
|
||||
const param = paramList.namedChild(i);
|
||||
if (!param) continue;
|
||||
|
||||
switch (param.type) {
|
||||
case 'required_parameter': {
|
||||
const patternNode = param.childForFieldName('pattern');
|
||||
if (!patternNode) break;
|
||||
|
||||
// Skip TS `this` parameter — it's a compile-time type constraint, not a real param
|
||||
if (patternNode.type === 'this') break;
|
||||
|
||||
// Rest parameter: pattern is a rest_pattern (...args) — extract inner identifier
|
||||
const isRest = patternNode.type === 'rest_pattern';
|
||||
const nameNode = isRest ? patternNode.firstNamedChild : patternNode;
|
||||
if (!nameNode) break;
|
||||
|
||||
// type field is a type_annotation — unwrap to get the inner type node
|
||||
const typeAnnotation = param.childForFieldName('type');
|
||||
const typeNode = typeAnnotation?.firstNamedChild;
|
||||
|
||||
// Default value: presence of a 'value' field means isOptional
|
||||
const hasDefault = !!param.childForFieldName('value');
|
||||
|
||||
params.push({
|
||||
name: nameNode.text,
|
||||
type: typeNode
|
||||
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
|
||||
: null,
|
||||
isOptional: hasDefault,
|
||||
isVariadic: isRest,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'optional_parameter': {
|
||||
const nameNode = param.childForFieldName('pattern');
|
||||
if (!nameNode) break;
|
||||
const typeAnnotation = param.childForFieldName('type');
|
||||
const typeNode = typeAnnotation?.firstNamedChild;
|
||||
params.push({
|
||||
name: nameNode.text,
|
||||
type: typeNode
|
||||
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
|
||||
: null,
|
||||
isOptional: true,
|
||||
isVariadic: false,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'rest_parameter': {
|
||||
const nameNode = param.childForFieldName('pattern');
|
||||
if (!nameNode) break;
|
||||
const typeAnnotation = param.childForFieldName('type');
|
||||
const typeNode = typeAnnotation?.firstNamedChild;
|
||||
params.push({
|
||||
name: nameNode.text,
|
||||
type: typeNode
|
||||
? (extractSimpleTypeName(typeNode) ?? typeNode.text?.trim() ?? null)
|
||||
: null,
|
||||
isOptional: false,
|
||||
isVariadic: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'identifier': {
|
||||
// JS: bare parameter name, no type info
|
||||
params.push({ name: param.text, type: null, isOptional: false, isVariadic: false });
|
||||
break;
|
||||
}
|
||||
case 'assignment_pattern': {
|
||||
// JS: param = defaultValue — the left side is the name, isOptional = true
|
||||
const left = param.childForFieldName('left');
|
||||
if (left) {
|
||||
params.push({ name: left.text, type: null, isOptional: true, isVariadic: false });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'rest_pattern': {
|
||||
// JS: ...args
|
||||
const inner = param.firstNamedChild;
|
||||
if (inner) {
|
||||
params.push({ name: inner.text, type: null, isOptional: false, isVariadic: true });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'object_pattern':
|
||||
case 'array_pattern': {
|
||||
// Destructured parameter — use full text as name
|
||||
params.push({ name: param.text, type: null, isOptional: false, isVariadic: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract return type from return_type field, unwrapping type_annotation.
|
||||
*
|
||||
* tree-sitter-typescript uses `return_type` as the field name (not `type` like JVM).
|
||||
* The return_type field points to a type_annotation node that must be unwrapped.
|
||||
*/
|
||||
function extractTsJsReturnType(node: SyntaxNode): string | undefined {
|
||||
const returnType = node.childForFieldName('return_type');
|
||||
if (returnType) {
|
||||
if (returnType.type === 'type_annotation') {
|
||||
const inner = returnType.firstNamedChild;
|
||||
if (inner) return extractSimpleTypeName(inner) ?? inner.text?.trim();
|
||||
}
|
||||
return extractSimpleTypeName(returnType) ?? returnType.text?.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract visibility from accessibility_modifier or #private name.
|
||||
*
|
||||
* tree-sitter-typescript emits accessibility_modifier as a named child of method nodes
|
||||
* (not as a modifiers wrapper like JVM). Pass 1 scans for that child; pass 2 checks for
|
||||
* ES2022 private_property_identifier (#name). Default: public.
|
||||
*/
|
||||
function extractTsJsVisibility(node: SyntaxNode): MethodVisibility {
|
||||
// Pass 1: check for accessibility_modifier named child (TS-specific)
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child && child.type === 'accessibility_modifier') {
|
||||
const t = child.text.trim();
|
||||
if (VISIBILITY_KEYWORDS.has(t as MethodVisibility)) return t as MethodVisibility;
|
||||
}
|
||||
}
|
||||
// Pass 2: ES2022 private methods (#name) are inherently private
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (nameNode && nameNode.type === 'private_property_identifier') return 'private';
|
||||
// No accessibility_modifier found — default to public.
|
||||
// Note: tree-sitter-typescript does not wrap modifiers in a 'modifiers' node
|
||||
// (unlike JVM), so there is no wrapper to scan.
|
||||
return 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract decorator names, prefixed with '@'.
|
||||
*
|
||||
* In tree-sitter-typescript, decorators are **siblings** of the method_definition in the
|
||||
* class_body — they are NOT children of the method node. We find them by walking backwards
|
||||
* from the method node through its preceding siblings in the parent body.
|
||||
*/
|
||||
function extractTsJsDecorators(node: SyntaxNode): string[] {
|
||||
const decorators: string[] = [];
|
||||
// Walk backwards via previousNamedSibling to collect consecutive decorator siblings.
|
||||
// This avoids the O(N) index-finding scan through the parent's children.
|
||||
let sibling = node.previousNamedSibling;
|
||||
while (sibling && sibling.type === 'decorator') {
|
||||
const name = extractDecoratorName(sibling);
|
||||
if (name) decorators.unshift(name);
|
||||
sibling = sibling.previousNamedSibling;
|
||||
}
|
||||
return decorators;
|
||||
}
|
||||
|
||||
function extractDecoratorName(decorator: SyntaxNode): string | undefined {
|
||||
const expr = decorator.firstNamedChild;
|
||||
if (!expr) return undefined;
|
||||
if (expr.type === 'call_expression') {
|
||||
const fn = expr.childForFieldName('function');
|
||||
return fn ? '@' + fn.text : undefined;
|
||||
}
|
||||
if (expr.type === 'identifier') return '@' + expr.text;
|
||||
if (expr.type === 'member_expression') return '@' + expr.text;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// TS and JS share the same config base. TS-only node types (abstract_class_declaration,
|
||||
// interface_declaration, abstract_method_signature, method_signature, interface_body) are
|
||||
// included because the JS grammar never produces these nodes — they are harmless no-ops.
|
||||
// This mirrors the field extractor's typescript-javascript.ts shared pattern.
|
||||
//
|
||||
// Note: TS and JS share a method config but NOT a field extractor because the TS field
|
||||
// extractor needs a hand-written class for type_alias_declaration object literals and
|
||||
// nested type discovery. Methods have no such requirement.
|
||||
const shared: Omit<MethodExtractionConfig, 'language'> = {
|
||||
typeDeclarationNodes: [
|
||||
'class_declaration',
|
||||
'abstract_class_declaration',
|
||||
'interface_declaration',
|
||||
],
|
||||
// Note: TS constructors are method_definition nodes (name = 'constructor'), so no
|
||||
// explicit constructor_declaration entry is needed (unlike JVM/C# configs).
|
||||
// Known gaps:
|
||||
// - call_signature and construct_signature (e.g., interface Fn { (x: string): void; })
|
||||
// are not extracted — they have no name field and are uncommon in practice.
|
||||
// - class_expression (const Foo = class { ... }) — methods inside class expressions
|
||||
// are not discovered because class_expression is not in typeDeclarationNodes.
|
||||
// - declare module / declare global augmentations — methods inside ambient_module_declaration
|
||||
// wrappers are not surfaced because the top-level walker doesn't descend into them.
|
||||
methodNodeTypes: ['method_definition', 'method_signature', 'abstract_method_signature'],
|
||||
bodyNodeTypes: ['class_body', 'interface_body'],
|
||||
|
||||
extractName(node) {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
return nameNode?.text;
|
||||
},
|
||||
|
||||
extractReturnType: extractTsJsReturnType,
|
||||
extractParameters: extractTsJsParameters,
|
||||
extractVisibility: extractTsJsVisibility,
|
||||
|
||||
isStatic(node) {
|
||||
return hasKeyword(node, 'static');
|
||||
},
|
||||
|
||||
isAbstract(node, ownerNode) {
|
||||
// Explicit abstract keyword on the method itself
|
||||
if (hasKeyword(node, 'abstract')) return true;
|
||||
// Interface methods are implicitly abstract — TS interfaces never have method bodies
|
||||
// (unlike Java default methods), so no !body check needed
|
||||
if (ownerNode.type === 'interface_declaration') return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
isFinal(_node) {
|
||||
return false; // TS/JS has no final/sealed methods
|
||||
},
|
||||
|
||||
extractAnnotations: extractTsJsDecorators,
|
||||
|
||||
isAsync(node) {
|
||||
return hasKeyword(node, 'async');
|
||||
},
|
||||
|
||||
isOverride(node) {
|
||||
return hasKeyword(node, 'override');
|
||||
},
|
||||
};
|
||||
|
||||
export const typescriptMethodConfig: MethodExtractionConfig = {
|
||||
...shared,
|
||||
language: SupportedLanguages.TypeScript,
|
||||
};
|
||||
|
||||
export const javascriptMethodConfig: MethodExtractionConfig = {
|
||||
...shared,
|
||||
language: SupportedLanguages.JavaScript,
|
||||
};
|
||||
@@ -24,6 +24,18 @@ export const TYPESCRIPT_QUERIES = `
|
||||
(method_definition
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
; ES2022 #private methods (private_property_identifier not matched by property_identifier)
|
||||
(method_definition
|
||||
name: (private_property_identifier) @name) @definition.method
|
||||
|
||||
; Abstract method signatures in abstract classes
|
||||
(abstract_method_signature
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
; Interface method signatures
|
||||
(method_signature
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @name
|
||||
@@ -145,6 +157,10 @@ export const JAVASCRIPT_QUERIES = `
|
||||
(method_definition
|
||||
name: (property_identifier) @name) @definition.method
|
||||
|
||||
; ES2022 #private methods
|
||||
(method_definition
|
||||
name: (private_property_identifier) @name) @definition.method
|
||||
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
name: (identifier) @name
|
||||
|
||||
@@ -664,6 +664,12 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// TypeScript: `this` parameter is a compile-time type constraint, not a real param
|
||||
// e.g., handle(this: void, event: Event) — only count 'event'
|
||||
if (param.type === 'required_parameter') {
|
||||
const patternNode = param.childForFieldName('pattern');
|
||||
if (patternNode?.type === 'this') continue;
|
||||
}
|
||||
// Kotlin: default values are siblings of the parameter node inside
|
||||
// function_value_parameters, so they appear as named children (e.g.
|
||||
// string_literal, integer_literal, boolean_literal, call_expression).
|
||||
|
||||
@@ -67,10 +67,12 @@ function buildHTML(
|
||||
pages: Record<string, string>,
|
||||
meta: Record<string, unknown> | null,
|
||||
): string {
|
||||
// Embed data as JSON inside the HTML
|
||||
const pagesJSON = JSON.stringify(pages);
|
||||
const treeJSON = JSON.stringify(moduleTree);
|
||||
const metaJSON = JSON.stringify(meta);
|
||||
// Embed data as JSON inside the HTML.
|
||||
// Escape </script> sequences so they don't prematurely close the <script> tag.
|
||||
const escScript = (s: string) => s.replace(/<\//g, '<\\/');
|
||||
const pagesJSON = escScript(JSON.stringify(pages));
|
||||
const treeJSON = escScript(JSON.stringify(moduleTree));
|
||||
const metaJSON = escScript(JSON.stringify(meta));
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
|
||||
@@ -152,11 +152,11 @@ export async function callLLM(
|
||||
messages,
|
||||
};
|
||||
|
||||
if (reasoning) {
|
||||
body.max_completion_tokens = config.maxTokens;
|
||||
// Do NOT include temperature, top_p, presence_penalty, frequency_penalty
|
||||
} else {
|
||||
body.max_tokens = config.maxTokens;
|
||||
// max_tokens is deprecated; use max_completion_tokens for all models
|
||||
body.max_completion_tokens = config.maxTokens;
|
||||
|
||||
// Only send temperature for non-Azure providers — some Azure models reject non-default values
|
||||
if (!reasoning && !azure && config.temperature !== undefined) {
|
||||
body.temperature = config.temperature;
|
||||
}
|
||||
|
||||
|
||||
@@ -535,6 +535,96 @@ public:
|
||||
});
|
||||
});
|
||||
|
||||
describe('HAS_METHOD integration — TypeScript: abstract, interface, and #private methods', () => {
|
||||
beforeAll(async () => {
|
||||
await loadLanguage(SupportedLanguages.TypeScript, 'methods.ts');
|
||||
});
|
||||
|
||||
it('abstract method signatures are captured and link to abstract class', () => {
|
||||
const code = `
|
||||
abstract class Shape {
|
||||
abstract area(): number;
|
||||
describe(): string { return "shape"; }
|
||||
}
|
||||
`;
|
||||
const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/shapes.ts');
|
||||
|
||||
const area = results.find((r) => r.name === 'area' && r.defType === 'definition.method');
|
||||
expect(area).toBeDefined();
|
||||
expect(area!.enclosingClassId).toBe('Class:src/shapes.ts:Shape');
|
||||
|
||||
const describe = results.find(
|
||||
(r) => r.name === 'describe' && r.defType === 'definition.method',
|
||||
);
|
||||
expect(describe).toBeDefined();
|
||||
expect(describe!.enclosingClassId).toBe('Class:src/shapes.ts:Shape');
|
||||
});
|
||||
|
||||
it('interface method signatures are captured and link to interface', () => {
|
||||
const code = `
|
||||
interface Printable {
|
||||
print(format: string): void;
|
||||
getLabel(): string;
|
||||
}
|
||||
`;
|
||||
const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/printable.ts');
|
||||
|
||||
const print = results.find((r) => r.name === 'print' && r.defType === 'definition.method');
|
||||
expect(print).toBeDefined();
|
||||
expect(print!.enclosingClassId).toBe('Interface:src/printable.ts:Printable');
|
||||
|
||||
const getLabel = results.find(
|
||||
(r) => r.name === 'getLabel' && r.defType === 'definition.method',
|
||||
);
|
||||
expect(getLabel).toBeDefined();
|
||||
expect(getLabel!.enclosingClassId).toBe('Interface:src/printable.ts:Printable');
|
||||
});
|
||||
|
||||
it('ES2022 #private methods are captured and link to class', () => {
|
||||
const code = `
|
||||
class Vault {
|
||||
#decrypt(data: string): string { return data; }
|
||||
read(): string { return this.#decrypt("x"); }
|
||||
}
|
||||
`;
|
||||
const results = parseAndExtractMethods(code, SupportedLanguages.TypeScript, 'src/vault.ts');
|
||||
|
||||
const decrypt = results.find((r) => r.name === '#decrypt');
|
||||
expect(decrypt).toBeDefined();
|
||||
expect(decrypt!.defType).toBe('definition.method');
|
||||
expect(decrypt!.enclosingClassId).toBe('Class:src/vault.ts:Vault');
|
||||
|
||||
const read = results.find((r) => r.name === 'read');
|
||||
expect(read).toBeDefined();
|
||||
expect(read!.enclosingClassId).toBe('Class:src/vault.ts:Vault');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HAS_METHOD integration — JavaScript: ES2022 #private methods', () => {
|
||||
beforeAll(async () => {
|
||||
await loadLanguage(SupportedLanguages.JavaScript);
|
||||
});
|
||||
|
||||
it('#private methods are captured and link to class', () => {
|
||||
const code = `
|
||||
class Encapsulated {
|
||||
#internal() { return 42; }
|
||||
expose() { return this.#internal(); }
|
||||
}
|
||||
`;
|
||||
const results = parseAndExtractMethods(
|
||||
code,
|
||||
SupportedLanguages.JavaScript,
|
||||
'src/encapsulated.js',
|
||||
);
|
||||
|
||||
const internal = results.find((r) => r.name === '#internal');
|
||||
expect(internal).toBeDefined();
|
||||
expect(internal!.defType).toBe('definition.method');
|
||||
expect(internal!.enclosingClassId).toBe('Class:src/encapsulated.js:Encapsulated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HAS_METHOD integration — C# struct and record', () => {
|
||||
beforeAll(async () => {
|
||||
await loadLanguage(SupportedLanguages.CSharp);
|
||||
|
||||
@@ -58,10 +58,11 @@ describe('TypeScript heritage resolution', () => {
|
||||
|
||||
it('emits HAS_METHOD edges linking methods to classes', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
expect(hasMethod.length).toBe(4);
|
||||
expect(hasMethod.length).toBe(5);
|
||||
expect(edgeSet(hasMethod)).toEqual([
|
||||
'BaseService → getName',
|
||||
'ConsoleLogger → log',
|
||||
'ILogger → log',
|
||||
'UserService → getUsers',
|
||||
'UserService → log',
|
||||
]);
|
||||
|
||||
@@ -511,6 +511,25 @@ describe('GenericFieldExtractor — TypeScript config', () => {
|
||||
expect(countField!.isStatic).toBe(false);
|
||||
expect(countField!.isReadonly).toBe(false);
|
||||
});
|
||||
|
||||
it('does not false-positive visibility on fields named after visibility keywords', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
// A class field literally named 'private' with no accessibility modifier —
|
||||
// findVisibility must not confuse the name with a keyword
|
||||
const tree = parser.parse(`
|
||||
class Flags {
|
||||
private: boolean;
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0);
|
||||
const result = extractor.extract(classNode!, mockContext);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.fields).toHaveLength(1);
|
||||
expect(result!.fields[0].name).toBe('private');
|
||||
// Default visibility is public — the field NAME 'private' must not be treated as a keyword
|
||||
expect(result!.fields[0].visibility).toBe('public');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -5,10 +5,16 @@ import {
|
||||
kotlinMethodConfig,
|
||||
} from '../../src/core/ingestion/method-extractors/configs/jvm.js';
|
||||
import { csharpMethodConfig } from '../../src/core/ingestion/method-extractors/configs/csharp.js';
|
||||
import {
|
||||
typescriptMethodConfig,
|
||||
javascriptMethodConfig,
|
||||
} from '../../src/core/ingestion/method-extractors/configs/typescript-javascript.js';
|
||||
import type { MethodExtractorContext } from '../../src/core/ingestion/method-types.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import Java from 'tree-sitter-java';
|
||||
import CSharp from 'tree-sitter-c-sharp';
|
||||
import TypeScript from 'tree-sitter-typescript';
|
||||
import JavaScript from 'tree-sitter-javascript';
|
||||
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
||||
|
||||
let Kotlin: unknown;
|
||||
@@ -295,7 +301,7 @@ describe('Java MethodExtractor', () => {
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, javaCtx);
|
||||
|
||||
expect(result!.methods.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
const sg = result!.methods.find((m) => m.name === 'surfaceGravity');
|
||||
expect(sg).toBeDefined();
|
||||
expect(sg!.returnType).toBe('double');
|
||||
@@ -1297,3 +1303,608 @@ describe('C# MethodExtractor', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TypeScript
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const parseTypeScript = (code: string) => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
return parser.parse(code);
|
||||
};
|
||||
|
||||
const tsCtx: MethodExtractorContext = {
|
||||
filePath: 'Test.ts',
|
||||
language: SupportedLanguages.TypeScript,
|
||||
};
|
||||
|
||||
describe('TypeScript MethodExtractor', () => {
|
||||
const extractor = createMethodExtractor(typescriptMethodConfig);
|
||||
|
||||
describe('isTypeDeclaration', () => {
|
||||
it('recognizes class_declaration', () => {
|
||||
const tree = parseTypeScript('class Foo { }');
|
||||
expect(extractor.isTypeDeclaration(tree.rootNode.child(0)!)).toBe(true);
|
||||
});
|
||||
|
||||
it('recognizes abstract_class_declaration', () => {
|
||||
const tree = parseTypeScript('abstract class Foo { }');
|
||||
expect(extractor.isTypeDeclaration(tree.rootNode.child(0)!)).toBe(true);
|
||||
});
|
||||
|
||||
it('recognizes interface_declaration', () => {
|
||||
const tree = parseTypeScript('interface Bar { }');
|
||||
expect(extractor.isTypeDeclaration(tree.rootNode.child(0)!)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects function_declaration', () => {
|
||||
const tree = parseTypeScript('function hello() {}');
|
||||
expect(extractor.isTypeDeclaration(tree.rootNode.child(0)!)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extract', () => {
|
||||
it('extracts typed method with return type and parameters', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class UserService {
|
||||
greet(name: string, age: number): string {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.ownerName).toBe('UserService');
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
|
||||
const m = result!.methods[0];
|
||||
expect(m.name).toBe('greet');
|
||||
expect(m.returnType).toBe('string');
|
||||
expect(m.visibility).toBe('public');
|
||||
expect(m.isStatic).toBe(false);
|
||||
expect(m.isAbstract).toBe(false);
|
||||
expect(m.parameters).toHaveLength(2);
|
||||
expect(m.parameters[0]).toEqual({
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
expect(m.parameters[1]).toEqual({
|
||||
name: 'age',
|
||||
type: 'number',
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts static method', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class MathUtils {
|
||||
static add(a: number, b: number): number { return a + b; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods[0].isStatic).toBe(true);
|
||||
expect(result!.methods[0].name).toBe('add');
|
||||
});
|
||||
|
||||
it('extracts abstract class with abstract and concrete methods', () => {
|
||||
const tree = parseTypeScript(`
|
||||
abstract class Shape {
|
||||
abstract area(): number;
|
||||
describe(): string { return "shape"; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.methods).toHaveLength(2);
|
||||
|
||||
const abstractMethod = result!.methods.find((m) => m.name === 'area');
|
||||
const concreteMethod = result!.methods.find((m) => m.name === 'describe');
|
||||
expect(abstractMethod!.isAbstract).toBe(true);
|
||||
expect(abstractMethod!.returnType).toBe('number');
|
||||
expect(concreteMethod!.isAbstract).toBe(false);
|
||||
expect(concreteMethod!.returnType).toBe('string');
|
||||
});
|
||||
|
||||
it('extracts interface methods as abstract', () => {
|
||||
const tree = parseTypeScript(`
|
||||
interface Printable {
|
||||
print(format: string): void;
|
||||
getLabel(): string;
|
||||
}
|
||||
`);
|
||||
const interfaceNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(interfaceNode, tsCtx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.ownerName).toBe('Printable');
|
||||
expect(result!.methods).toHaveLength(2);
|
||||
expect(result!.methods.every((m) => m.isAbstract)).toBe(true);
|
||||
|
||||
const printMethod = result!.methods.find((m) => m.name === 'print');
|
||||
expect(printMethod!.parameters[0]).toEqual({
|
||||
name: 'format',
|
||||
type: 'string',
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
expect(printMethod!.returnType).toBe('void');
|
||||
});
|
||||
|
||||
it('extracts private and protected visibility', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Account {
|
||||
private secret(): void {}
|
||||
protected validate(): boolean { return true; }
|
||||
public display(): void {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(3);
|
||||
const secret = result!.methods.find((m) => m.name === 'secret');
|
||||
const validate = result!.methods.find((m) => m.name === 'validate');
|
||||
const display = result!.methods.find((m) => m.name === 'display');
|
||||
expect(secret!.visibility).toBe('private');
|
||||
expect(validate!.visibility).toBe('protected');
|
||||
expect(display!.visibility).toBe('public');
|
||||
});
|
||||
|
||||
it('extracts optional and rest parameters', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Logger {
|
||||
log(message: string, level?: string, ...tags: string[]): void {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
expect(params).toHaveLength(3);
|
||||
expect(params[0]).toEqual({
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
expect(params[1].name).toBe('level');
|
||||
expect(params[1].isOptional).toBe(true);
|
||||
expect(params[1].isVariadic).toBe(false);
|
||||
expect(params[2].name).toBe('tags');
|
||||
expect(params[2].isOptional).toBe(false);
|
||||
expect(params[2].isVariadic).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts default parameter as optional', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Formatter {
|
||||
format(value: string, prefix: string = ">>") { return prefix + value; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
expect(params).toHaveLength(2);
|
||||
expect(params[1].name).toBe('prefix');
|
||||
expect(params[1].isOptional).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts decorators as annotations', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Controller {
|
||||
@Log
|
||||
@deprecated("use newMethod")
|
||||
handle(req: Request): void {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const annotations = result!.methods[0].annotations;
|
||||
expect(annotations).toContain('@Log');
|
||||
expect(annotations).toContain('@deprecated');
|
||||
});
|
||||
|
||||
it('extracts async method', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class ApiClient {
|
||||
async fetch(url: string): Promise<Response> { return new Response(); }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods[0].isAsync).toBe(true);
|
||||
expect(result!.methods[0].name).toBe('fetch');
|
||||
expect(result!.methods[0].returnType).toBe('Promise');
|
||||
});
|
||||
|
||||
it('extracts constructor', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Person {
|
||||
constructor(public name: string, private age: number) {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
const ctor = result!.methods[0];
|
||||
expect(ctor.name).toBe('constructor');
|
||||
expect(ctor.parameters).toHaveLength(2);
|
||||
expect(ctor.parameters[0].name).toBe('name');
|
||||
expect(ctor.parameters[0].type).toBe('string');
|
||||
expect(ctor.parameters[1].name).toBe('age');
|
||||
expect(ctor.parameters[1].type).toBe('number');
|
||||
});
|
||||
|
||||
it('extracts override method', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Child extends Parent {
|
||||
override toString(): string { return "child"; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods[0].name).toBe('toString');
|
||||
expect(result!.methods[0].isOverride).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts getter and setter as methods', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Config {
|
||||
get value(): number { return 1; }
|
||||
set value(v: number) {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
// Getter and setter both have name 'value' (no get/set prefix from extractName)
|
||||
expect(result!.methods).toHaveLength(2);
|
||||
const getter = result!.methods[0];
|
||||
const setter = result!.methods[1];
|
||||
expect(getter.name).toBe('value');
|
||||
expect(getter.parameters).toHaveLength(0);
|
||||
expect(getter.returnType).toBe('number');
|
||||
expect(setter.name).toBe('value');
|
||||
expect(setter.parameters).toHaveLength(1);
|
||||
expect(setter.parameters[0].name).toBe('v');
|
||||
});
|
||||
|
||||
it('extracts destructured parameter', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Handler {
|
||||
handle({ method, path }: Request): void {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
expect(params).toHaveLength(1);
|
||||
// Destructured params extract the pattern text and type from annotation
|
||||
expect(params[0].name).toBe('{ method, path }');
|
||||
expect(params[0].type).toBe('Request');
|
||||
});
|
||||
|
||||
it('extracts generator method as method_definition', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Stream {
|
||||
*items(): Generator<number> { yield 1; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
expect(result!.methods[0].name).toBe('items');
|
||||
expect(result!.methods[0].returnType).toBe('Generator');
|
||||
});
|
||||
|
||||
it('extracts async generator method with isAsync true', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Stream {
|
||||
async *values(): AsyncGenerator<number> { yield 1; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
expect(result!.methods[0].name).toBe('values');
|
||||
expect(result!.methods[0].isAsync).toBe(true);
|
||||
expect(result!.methods[0].returnType).toBe('AsyncGenerator');
|
||||
});
|
||||
|
||||
it('extracts computed property name with brackets', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Iterable {
|
||||
[Symbol.iterator](): Iterator<number> { return this; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
// Computed names include brackets — this is intentional for static analysis disambiguation
|
||||
expect(result!.methods[0].name).toBe('[Symbol.iterator]');
|
||||
});
|
||||
|
||||
it('extracts class-level method overloads', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Parser {
|
||||
parse(input: string): string;
|
||||
parse(input: number): number;
|
||||
parse(input: string | number): string | number { return input; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
// Two overload signatures (method_signature) + one implementation (method_definition) = 3
|
||||
const parseMethods = result!.methods.filter((m) => m.name === 'parse');
|
||||
expect(parseMethods).toHaveLength(3);
|
||||
// Overload signatures inside a class body are not abstract
|
||||
for (const m of parseMethods) {
|
||||
expect(m.isAbstract).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('filters out this-parameter (compile-time constraint)', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Handler {
|
||||
handle(this: void, event: Event): void {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
// 'this' is not a real parameter — only 'event' should appear
|
||||
expect(params).toHaveLength(1);
|
||||
expect(params[0].name).toBe('event');
|
||||
expect(params[0].type).toBe('Event');
|
||||
});
|
||||
|
||||
it('does not false-positive on methods named after soft keywords', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Foo {
|
||||
static abstract() {}
|
||||
static() {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const abstractMethod = result!.methods.find((m) => m.name === 'abstract');
|
||||
expect(abstractMethod).toBeDefined();
|
||||
expect(abstractMethod!.isStatic).toBe(true);
|
||||
expect(abstractMethod!.isAbstract).toBe(false); // name, not keyword
|
||||
|
||||
const staticMethod = result!.methods.find((m) => m.name === 'static');
|
||||
expect(staticMethod).toBeDefined();
|
||||
expect(staticMethod!.isStatic).toBe(false); // name, not keyword
|
||||
});
|
||||
|
||||
it('extracts destructured rest parameter via required_parameter + rest_pattern', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Router {
|
||||
route(base: string, ...{ method, path }: RouteConfig): void {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
expect(params).toHaveLength(2);
|
||||
expect(params[0].name).toBe('base');
|
||||
expect(params[1].name).toBe('{ method, path }');
|
||||
expect(params[1].isVariadic).toBe(true);
|
||||
expect(params[1].type).toBe('RouteConfig');
|
||||
});
|
||||
|
||||
it('extracts ES2022 #private method as visibility private', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Vault {
|
||||
#decrypt(data: string): string { return data; }
|
||||
public read(): string { return this.#decrypt("x"); }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const decrypt = result!.methods.find((m) => m.name === '#decrypt');
|
||||
expect(decrypt).toBeDefined();
|
||||
expect(decrypt!.visibility).toBe('private');
|
||||
expect(decrypt!.parameters[0].type).toBe('string');
|
||||
|
||||
const read = result!.methods.find((m) => m.name === 'read');
|
||||
expect(read!.visibility).toBe('public');
|
||||
});
|
||||
|
||||
it('extracts generic method without type params in name', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Mapper {
|
||||
transform<T, U>(input: T, fn: (x: T) => U): U { return fn(input); }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
const m = result!.methods[0];
|
||||
expect(m.name).toBe('transform');
|
||||
expect(m.parameters).toHaveLength(2);
|
||||
expect(m.parameters[0].name).toBe('input');
|
||||
expect(m.parameters[0].type).toBe('T');
|
||||
});
|
||||
|
||||
it('returns empty methods for class with no methods', () => {
|
||||
const tree = parseTypeScript(`
|
||||
class Empty {}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, tsCtx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.ownerName).toBe('Empty');
|
||||
expect(result!.methods).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JavaScript
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const parseJavaScript = (code: string) => {
|
||||
parser.setLanguage(JavaScript);
|
||||
return parser.parse(code);
|
||||
};
|
||||
|
||||
const jsCtx: MethodExtractorContext = {
|
||||
filePath: 'Test.js',
|
||||
language: SupportedLanguages.JavaScript,
|
||||
};
|
||||
|
||||
describe('JavaScript MethodExtractor', () => {
|
||||
const extractor = createMethodExtractor(javascriptMethodConfig);
|
||||
|
||||
describe('extract', () => {
|
||||
it('extracts class method with default public visibility and null types', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class Greeter {
|
||||
greet(name) { return "Hello " + name; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.ownerName).toBe('Greeter');
|
||||
expect(result!.methods).toHaveLength(1);
|
||||
|
||||
const m = result!.methods[0];
|
||||
expect(m.name).toBe('greet');
|
||||
expect(m.returnType).toBeNull();
|
||||
expect(m.visibility).toBe('public');
|
||||
expect(m.isAbstract).toBe(false);
|
||||
expect(m.parameters).toHaveLength(1);
|
||||
expect(m.parameters[0]).toEqual({
|
||||
name: 'name',
|
||||
type: null,
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts static method and constructor', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class Factory {
|
||||
constructor(type) { this.type = type; }
|
||||
static create(type) { return new Factory(type); }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
expect(result!.methods).toHaveLength(2);
|
||||
const ctor = result!.methods.find((m) => m.name === 'constructor');
|
||||
const create = result!.methods.find((m) => m.name === 'create');
|
||||
expect(ctor).toBeDefined();
|
||||
expect(create!.isStatic).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts default parameter as optional and rest as variadic', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class EventEmitter {
|
||||
emit(event, data = null, ...listeners) {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
expect(params).toHaveLength(3);
|
||||
expect(params[0]).toEqual({
|
||||
name: 'event',
|
||||
type: null,
|
||||
isOptional: false,
|
||||
isVariadic: false,
|
||||
});
|
||||
expect(params[1].name).toBe('data');
|
||||
expect(params[1].isOptional).toBe(true);
|
||||
expect(params[2].name).toBe('listeners');
|
||||
expect(params[2].isVariadic).toBe(true);
|
||||
});
|
||||
|
||||
it('does not detect abstract or interface types (JS has neither)', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class Shape {
|
||||
area() { return 0; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
expect(result!.methods[0].isAbstract).toBe(false);
|
||||
expect(result!.methods[0].isFinal).toBe(false);
|
||||
});
|
||||
|
||||
it('extracts private field method with # prefix', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class Encapsulated {
|
||||
#internal() { return 42; }
|
||||
expose() { return this.#internal(); }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
const internal = result!.methods.find((m) => m.name === '#internal');
|
||||
expect(internal).toBeDefined();
|
||||
expect(internal!.name).toBe('#internal');
|
||||
// ES2022 private methods (#name) are inherently private
|
||||
expect(internal!.visibility).toBe('private');
|
||||
});
|
||||
|
||||
it('extracts destructured object parameter', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class Handler {
|
||||
handle({ method, path }) {}
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
const params = result!.methods[0].parameters;
|
||||
expect(params).toHaveLength(1);
|
||||
expect(params[0].name).toBe('{ method, path }');
|
||||
expect(params[0].type).toBeNull();
|
||||
});
|
||||
|
||||
it('extracts async method', () => {
|
||||
const tree = parseJavaScript(`
|
||||
class Client {
|
||||
async fetch(url) { return null; }
|
||||
}
|
||||
`);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const result = extractor.extract(classNode, jsCtx);
|
||||
|
||||
expect(result!.methods[0].isAsync).toBe(true);
|
||||
expect(result!.methods[0].name).toBe('fetch');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,6 +64,31 @@ describe('extractMethodSignature', () => {
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
expect(sig.returnType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('skips TypeScript this-parameter (compile-time constraint)', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `class Handler {
|
||||
handle(this: void, event: Event): void {}
|
||||
}`;
|
||||
const tree = parser.parse(code);
|
||||
const classNode = tree.rootNode.child(0)!;
|
||||
const classBody = classNode.childForFieldName('body')!;
|
||||
const methodNode = classBody.namedChild(0)!;
|
||||
|
||||
const sig = extractMethodSignature(methodNode);
|
||||
// 'this' is not a real parameter — only 'event' should be counted
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
});
|
||||
|
||||
it('skips this-parameter in top-level function', () => {
|
||||
parser.setLanguage(TypeScript.typescript);
|
||||
const code = `function onClick(this: HTMLElement, ev: MouseEvent): void {}`;
|
||||
const tree = parser.parse(code);
|
||||
const funcNode = tree.rootNode.child(0)!;
|
||||
|
||||
const sig = extractMethodSignature(funcNode);
|
||||
expect(sig.parameterCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python', () => {
|
||||
|
||||
@@ -207,7 +207,7 @@ describe('callLLM — reasoning model params', () => {
|
||||
expect(body.temperature).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses max_tokens and temperature for non-reasoning models', async () => {
|
||||
it('uses max_completion_tokens and temperature for non-reasoning models', async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ choices: [{ message: { content: 'answer' } }], usage: {} }), {
|
||||
status: 200,
|
||||
@@ -230,8 +230,8 @@ describe('callLLM — reasoning model params', () => {
|
||||
RequestInit & { headers: Record<string, string> },
|
||||
];
|
||||
const body = JSON.parse(init.body as string);
|
||||
expect(body.max_tokens).toBe(500);
|
||||
expect(body.max_completion_tokens).toBeUndefined();
|
||||
expect(body.max_completion_tokens).toBe(500);
|
||||
expect(body.max_tokens).toBeUndefined();
|
||||
expect(body.temperature).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user