Compare commits

..
Author SHA1 Message Date
Abhigyan PatwariandClaude Opus 4.6 dc86ea96dc chore: release v1.5.3 — update CHANGELOG and package-lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:35:24 +05:30
Abhigyan PatwariandClaude Opus 4.6 d1adc8331a chore: release v1.6.0 — update CHANGELOG and package-lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:32:11 +05:30
80d363f145 fix(wiki): Azure OpenAI compat and HTML viewer script injection (#618)
* fix(wiki): Azure OpenAI compat and HTML viewer script injection

- Use max_completion_tokens instead of deprecated max_tokens for all models
- Skip sending temperature for Azure provider (some models reject non-default values)
- Simplify Azure interactive setup: endpoint + deployment + key (3 prompts instead of 7)
- Escape </script> in embedded JSON to prevent premature script tag closure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(test): align wiki-llm-client test with max_completion_tokens change

The test expected max_tokens for non-reasoning models, but the source
now uses max_completion_tokens for all models since max_tokens is
deprecated by newer OpenAI models.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Abhigyan Patwari <abhigyan@Abhigyans-MacBook-Air.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:30:43 +05:30
Gergő Magyar 12be2025f1 feat(ts,js): TypeScript/JavaScript MethodExtractor config (#588)
* feat(ts,js): MethodExtractor config for TypeScript and JavaScript (#570)

Add per-language method extraction config following the established
JVM and C# patterns. Shared config base mirrors the field extractor's
typescript-javascript.ts pattern — TS-only node types are harmless
no-ops for JS.

Key features:
- isAbstract for abstract class methods and interface methods
- Parameter extraction with isOptional (?:, defaults) and isVariadic (...)
- Decorator extraction from preceding body-level siblings
- isAsync and isOverride detection
- Visibility via accessibility_modifier two-pass pattern
- Return type extraction unwrapping type_annotation

* test(ts,js): add override, getter/setter, destructured param tests

Address code review findings:
- Add override method detection test
- Add getter/setter extraction test
- Add destructured parameter with type annotation test
- Tighten constructor and private method assertions

* refactor(ts,js): address code review findings

- Replace O(M*N) decorator index scan with previousNamedSibling walk
- Remove dead findVisibility 'modifiers' fallback (TS uses
  accessibility_modifier, not a modifiers wrapper)
- Document call_signature/construct_signature as known gaps
- Document that TS constructors are method_definition nodes
- Remove unused findVisibility import

* fix(ts,js): type guard before cast, add generator/computed/overload tests

- Use type guard pattern (Set.has check before as-cast) in visibility
  extraction to ensure string is validated before narrowing
- Add generator method test (*items()) — confirms extraction works
- Add computed property name test ([Symbol.iterator]) — documents
  bracket-in-name behavior as intentional
- Add class-level method overload test — verifies overload signatures
  + implementation are all extracted

* fix(ts,js): detect #private methods as visibility 'private'

ES2022 private class methods (#name) use private_property_identifier
as their name node type. Detect this and return 'private' visibility
instead of the default 'public'.

* fix(ts,js): address review findings + close ingestion gaps

- hasKeyword/findVisibility: skip name field child to prevent false
  positives on soft-keyword method names (e.g. `abstract()`, `static()`)
- extractTsJsParameters: filter TS `this` parameter (compile-time only)
- extractMethodSignature: mirror `this`-param skip in fallback path
- tree-sitter queries: capture abstract_method_signature,
  method_signature, and private_property_identifier for TS; add
  private_property_identifier for JS
- Remove dead childForFieldName('name') fallbacks and typeFromAnnotation
  fallback
- Add 10+ unit tests, 4 integration tests through query pipeline

* test(ts): update HAS_METHOD count for interface method_signature capture

The new method_signature query now captures ILogger.log() as a Method
node with a HAS_METHOD edge, increasing the expected count from 4 to 5.

* fix(ts,js): address second review — async generator test, declare module gap

- Add async generator method test (async *values() → isAsync: true)
- Document declare module/global augmentation as known gap
2026-04-01 14:09:59 +01:00
Abhigyan PatwariandClaude Opus 4.6 dcf40da3a8 fix: ensure import rewrites survive npm publish lifecycle
npm runs `prepare` after `prepack` during publish, so the previous
`prepare: tsc` overwrote the rewritten imports before packing.
Both `prepare` and `prepack` now run the full build script so the
tarball always contains rewritten relative imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:28:14 +05:30
Abhigyan PatwariandClaude Opus 4.6 c617350c58 chore: release v1.5.1 — update CHANGELOG and package-lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:05:08 +05:30
71255a096c fix: bundle gitnexus-shared into CLI dist (#613)
* fix(ci): build gitnexus-shared before publish, use CHANGELOG for release notes

The publish workflow was missing the gitnexus-shared build step that
the setup-gitnexus composite action provides. Since PR #536 unified
the ingestion pipeline, gitnexus imports types from gitnexus-shared,
so it must be built first.

Also replaces generate_release_notes with CHANGELOG.md extraction so
GitHub Releases use the reviewed changelog entry instead of a flat
PR title list.

Made-with: Cursor

* fix: bundle gitnexus-shared into CLI dist to fix module resolution

gitnexus-shared was declared as a file: dependency but never published
to npm, causing ERR_MODULE_NOT_FOUND for users installing gitnexus
globally. The build script now copies gitnexus-shared/dist into
dist/_shared/ and rewrites bare specifiers to relative paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move gitnexus-shared to devDependencies, use tsc for prepare

gitnexus-shared must remain available for tsc to resolve imports during
development/CI, but is not needed at runtime since it's bundled into
dist/_shared/. Moving it to devDependencies keeps it out of production
installs while allowing compilation. The prepare script now runs plain
tsc (no shared bundling needed for local dev).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Abhigyan Patwari <abhigyan@Abhigyans-MacBook-Air.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 17:04:39 +05:30
Gergo Magyar 39322e37df fix(ci): build gitnexus-shared before npm ci in publish workflow
The publish job failed because npm ci triggers the prepare script (tsc)
before gitnexus-shared types are available. Add the same build step
that the setup-gitnexus composite action uses in CI.
2026-04-01 07:35:46 +01:00
19 changed files with 1131 additions and 80 deletions
+13
View File
@@ -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
+11
View File
@@ -2,6 +2,17 @@
All notable changes to GitNexus will be documented in this file.
## [1.5.2] - 2026-04-01
### Fixed
- **`gitnexus-shared` module not found** — `gitnexus-shared` was a `file:` workspace dependency never published to npm, causing `ERR_MODULE_NOT_FOUND` when installing `gitnexus` globally. The build now bundles shared code into `dist/_shared/` and rewrites imports to relative paths (#613)
- **v1.5.1 publish regression** — npm's `prepare` lifecycle ran `tsc` after `prepack`, overwriting the rewritten imports before packing; both scripts now run the full build so the final tarball is always correct
## [1.5.1] - 2026-04-01 [YANKED]
### Fixed
- Incomplete fix for `gitnexus-shared` bundling — `prepare` script overwrote rewritten imports during publish
## [1.5.0] - 2026-04-01
### Added
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "gitnexus",
"version": "1.5.0",
"version": "1.5.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gitnexus",
"version": "1.5.0",
"version": "1.5.3",
"license": "PolyForm-Noncommercial-1.0.0",
"dependencies": {
"@huggingface/transformers": "^3.0.0",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "gitnexus",
"version": "1.5.0",
"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",
@@ -46,8 +46,8 @@
"test:integration": "vitest run test/integration",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"prepare": "tsc",
"prepack": "npm run build && chmod +x dist/cli/index.js"
"prepare": "node scripts/build.js",
"prepack": "node scripts/build.js"
},
"dependencies": {
"@huggingface/transformers": "^3.0.0",
+4
View File
@@ -66,4 +66,8 @@ function walk(dir, extensions, cb) {
walk(DIST, ['.js', '.d.ts'], rewriteFile);
// ── 5. Make CLI entry executable ────────────────────────────────────
const cliEntry = path.join(DIST, 'cli', 'index.js');
if (fs.existsSync(cliEntry)) fs.chmodSync(cliEntry, 0o755);
console.log(`[build] done — rewrote ${rewritten} files.`);
+18 -58
View File
@@ -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).
+6 -4
View File
@@ -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[] = [];
+5 -5
View File
@@ -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');
});
});
// ---------------------------------------------------------------------------
+612 -1
View File
@@ -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', () => {
+3 -3
View File
@@ -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);
});
});