refactor: reduce explicit any types (#566)

* refactor: replace NodeProperties index signature any with unknown

Change [key: string]: any to [key: string]: unknown in NodeProperties.
Remove 19 redundant (node.properties as any) casts in csv-generator.ts
— all accessed properties are already declared on the type.

* refactor: replace any with SyntaxNode across ingestion layer

Mechanical substitution — all tree-sitter AST node parameters and
variables typed as any are now properly typed as SyntaxNode.

- ast-helpers.ts: 13 any → SyntaxNode
- parsing-processor.ts: 8 any → SyntaxNode
- parse-worker.ts: 40 any → SyntaxNode/TreeSitterLanguage/Parser.Query
- php.ts: 11 any → SyntaxNode

Also adds TreeSitterLanguage type alias for optional grammar loading.

* refactor: eliminate remaining any in ingestion layer

- call-processor, call-routing, c-cpp: SyntaxNode substitutions
- parse-worker: typed WorkerIncomingMessage discriminated union
- worker-pool: typed WorkerOutgoingMessage + Error handler
- ast-cache, import-processor: targeted cast for Tree.delete()
- community-processor: graphology AbstractGraph types, LeidenModule
  interface for vendored leiden code

Ingestion layer: 130 → 7 any warnings remaining.
This commit is contained in:
Gergő Magyar 2026-03-28 17:11:24 +00:00 • committed by GitHub
parent 6fb2d0ab34
commit 2ca7df642b
12 changed files with 185 additions and 145 deletions

View file

@ -83,7 +83,7 @@ export type NodeProperties = {
errorKeys?: string[];
middleware?: string[];
// Extensible
[key: string]: any;
[key: string]: unknown;
};
export type RelationshipType =

View file

@ -19,7 +19,7 @@ export const createASTCache = (maxSize: number = 50): ASTCache => {
try {
// NOTE: web-tree-sitter has tree.delete(); native tree-sitter trees are GC-managed.
// Keep this try/catch so we don't crash on either runtime.
(tree as any).delete?.();
(tree as unknown as { delete?: () => void }).delete?.();
} catch (e) {
console.warn('Failed to delete tree from WASM memory', e);
}

View file

@ -72,8 +72,8 @@ export function routeRubyCall(calledName: string, callNode: SyntaxNode): RubyCal
// ── require / require_relative → import ─────────────────────────────────
if (calledName === 'require' || calledName === 'require_relative') {
const argList = callNode.childForFieldName?.('arguments');
const stringNode = argList?.children?.find((c: any) => c.type === 'string');
const contentNode = stringNode?.children?.find((c: any) => c.type === 'string_content');
const stringNode = argList?.children?.find((c: SyntaxNode) => c.type === 'string');
const contentNode = stringNode?.children?.find((c: SyntaxNode) => c.type === 'string_content');
if (!contentNode) return SKIP_RESULT;
let importPath: string = contentNode.text;

View file

@ -12,6 +12,7 @@
// (src/communities-leiden) because it was never published to npm.
// We use createRequire to load the CommonJS vendored files in ESM context.
import Graph from 'graphology';
import type { AbstractGraph, Attributes } from 'graphology-types';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
@ -23,7 +24,22 @@ const __dirname = dirname(__filename);
// Navigate to package root (works from both src/ and dist/)
const leidenPath = resolve(__dirname, '..', '..', '..', 'vendor', 'leiden', 'index.cjs');
const _require = createRequire(import.meta.url);
const leiden = _require(leidenPath);
/** Graphology Graph instance type (AbstractGraph from graphology-types avoids CJS/ESM interop namespace issue) */
type GraphInstance = AbstractGraph<Attributes, Attributes, Attributes>;
const leiden: LeidenModule = _require(leidenPath);
/** Vendored Leiden algorithm module shape */
interface LeidenModule {
detailed: (graph: GraphInstance, options: Record<string, unknown>) => LeidenDetailedResult;
}
/** Result returned by leiden.detailed() */
interface LeidenDetailedResult {
communities: Record<string, number>;
count: number;
modularity: number;
}
// ============================================================================
// TYPES
@ -127,16 +143,16 @@ export const processCommunities = async (
// The first 2 iterations capture ~95%+ of modularity; additional iterations have diminishing returns.
// Timeout: abort after 60s for pathological graph structures.
const LEIDEN_TIMEOUT_MS = 60_000;
let details: any;
let details: LeidenDetailedResult;
try {
details = await Promise.race([
Promise.resolve(
(leiden as any).detailed(graph, {
leiden.detailed(graph, {
resolution: isLarge ? 2.0 : 1.0,
maxIterations: isLarge ? 3 : 0,
}),
),
new Promise((_, reject) =>
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Leiden timeout')), LEIDEN_TIMEOUT_MS),
),
]);
@ -199,8 +215,12 @@ export const processCommunities = async (
*/
const MIN_CONFIDENCE_LARGE = 0.5;
const buildGraphologyGraph = (knowledgeGraph: KnowledgeGraph, isLarge: boolean): any => {
const graph = new (Graph as any)({ type: 'undirected', allowSelfLoops: false });
const buildGraphologyGraph = (knowledgeGraph: KnowledgeGraph, isLarge: boolean): GraphInstance => {
const GraphCtor = Graph as unknown as new (options: {
type: string;
allowSelfLoops: boolean;
}) => GraphInstance;
const graph = new GraphCtor({ type: 'undirected', allowSelfLoops: false });
const symbolTypes = new Set<NodeLabel>(['Function', 'Class', 'Method', 'Interface']);
const clusteringRelTypes = new Set(['CALLS', 'EXTENDS', 'IMPLEMENTS']);
@ -257,7 +277,7 @@ const buildGraphologyGraph = (knowledgeGraph: KnowledgeGraph, isLarge: boolean):
const createCommunityNodes = (
communities: Record<string, number>,
communityCount: number,
graph: any,
graph: GraphInstance,
knowledgeGraph: KnowledgeGraph,
): CommunityNode[] => {
// Group node IDs by community
@ -312,7 +332,7 @@ const createCommunityNodes = (
const generateHeuristicLabel = (
memberIds: string[],
nodePathMap: Map<string, string>,
graph: any,
graph: GraphInstance,
commNum: number,
): string => {
// Collect folder names from file paths
@ -397,7 +417,7 @@ const findCommonPrefix = (strings: string[]): string => {
* Estimate cohesion score (0-1) based on internal edge density.
* Uses sampling for large communities to avoid O(N^2) cost.
*/
const calculateCohesion = (memberIds: string[], graph: any): number => {
const calculateCohesion = (memberIds: string[], graph: GraphInstance): number => {
if (memberIds.length <= 1) return 1.0;
const memberSet = new Set(memberIds);

View file

@ -352,7 +352,7 @@ export const processImports = async (
console.groupEnd();
}
if (wasReparsed) (tree as any).delete?.();
if (wasReparsed) (tree as unknown as { delete?: () => void }).delete?.();
continue;
}

View file

@ -13,7 +13,7 @@ import { phpExportChecker } from '../export-detection.js';
import { resolvePhpImport } from '../import-resolvers/php.js';
import { extractPhpNamedBindings } from '../named-bindings/php.js';
import { PHP_QUERIES } from '../tree-sitter-queries.js';
import { findDescendant, extractStringContent } from '../utils/ast-helpers.js';
import { findDescendant, extractStringContent, type SyntaxNode } from '../utils/ast-helpers.js';
import type { NodeLabel } from 'gitnexus-shared';
import { createFieldExtractor } from '../field-extractors/generic.js';
import { phpConfig as phpFieldConfig } from '../field-extractors/configs/php.js';
@ -128,7 +128,7 @@ const ELOQUENT_RELATIONS = new Set([
* For a PHP property_declaration node, extract array values as a description string.
* Returns null if not an Eloquent model property or no array values found.
*/
function extractPhpPropertyDescription(propName: string, propDeclNode: any): string | null {
function extractPhpPropertyDescription(propName: string, propDeclNode: SyntaxNode): string | null {
if (!ELOQUENT_ARRAY_PROPS.has(propName)) return null;
const arrayNode = findDescendant(propDeclNode, 'array_creation_expression');
@ -138,7 +138,7 @@ function extractPhpPropertyDescription(propName: string, propDeclNode: any): str
for (const child of arrayNode.children ?? []) {
if (child.type !== 'array_element_initializer') continue;
const children = child.children ?? [];
const arrowIdx = children.findIndex((c: any) => c.type === '=>');
const arrowIdx = children.findIndex((c: SyntaxNode) => c.type === '=>');
if (arrowIdx !== -1) {
const key = extractStringContent(children[arrowIdx - 1]);
const val = extractStringContent(children[arrowIdx + 1]);
@ -156,14 +156,14 @@ function extractPhpPropertyDescription(propName: string, propDeclNode: any): str
* For a PHP method_declaration node, detect if it defines an Eloquent relationship.
* Returns description like "hasMany(Post)" or null.
*/
function extractEloquentRelationDescription(methodNode: any): string | null {
function findRelationCall(node: any): any {
function extractEloquentRelationDescription(methodNode: SyntaxNode): string | null {
function findRelationCall(node: SyntaxNode): SyntaxNode | null {
if (node.type === 'member_call_expression') {
const children = node.children ?? [];
const objectNode = children.find(
(c: any) => c.type === 'variable_name' && c.text === '$this',
(c: SyntaxNode) => c.type === 'variable_name' && c.text === '$this',
);
const nameNode = children.find((c: any) => c.type === 'name');
const nameNode = children.find((c: SyntaxNode) => c.type === 'name');
if (objectNode && nameNode && ELOQUENT_RELATIONS.has(nameNode.text)) return node;
}
for (const child of node.children ?? []) {
@ -176,17 +176,18 @@ function extractEloquentRelationDescription(methodNode: any): string | null {
const callNode = findRelationCall(methodNode);
if (!callNode) return null;
const relType = callNode.children?.find((c: any) => c.type === 'name')?.text;
const argsNode = callNode.children?.find((c: any) => c.type === 'arguments');
const relType = callNode.children?.find((c: SyntaxNode) => c.type === 'name')?.text;
const argsNode = callNode.children?.find((c: SyntaxNode) => c.type === 'arguments');
let targetModel: string | null = null;
if (argsNode) {
const firstArg = argsNode.children?.find((c: any) => c.type === 'argument');
const firstArg = argsNode.children?.find((c: SyntaxNode) => c.type === 'argument');
if (firstArg) {
const classConstant = firstArg.children?.find(
(c: any) => c.type === 'class_constant_access_expression',
(c: SyntaxNode) => c.type === 'class_constant_access_expression',
);
if (classConstant) {
targetModel = classConstant.children?.find((c: any) => c.type === 'name')?.text ?? null;
targetModel =
classConstant.children?.find((c: SyntaxNode) => c.type === 'name')?.text ?? null;
}
}
}
@ -203,7 +204,7 @@ function extractEloquentRelationDescription(methodNode: any): string | null {
function phpDescriptionExtractor(
nodeLabel: NodeLabel,
nodeName: string,
captureMap: Record<string, any>,
captureMap: Record<string, SyntaxNode>,
): string | undefined {
if (nodeLabel === 'Property' && captureMap['definition.property']) {
return extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;

View file

@ -1,4 +1,4 @@
import type { GraphNode, GraphRelationship } from 'gitnexus-shared';
import type { GraphNode, GraphRelationship, NodeLabel } from 'gitnexus-shared';
import { KnowledgeGraph } from '../graph/types.js';
import Parser from 'tree-sitter';
import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
@ -114,7 +114,7 @@ const processParsingWithWorkers = async (
for (const node of result.nodes) {
graph.addNode({
id: node.id,
label: node.label as any,
label: node.label as NodeLabel,
properties: node.properties,
});
}
@ -184,10 +184,10 @@ const processParsingWithWorkers = async (
// Inline caches to avoid repeated parent-walks per node (same pattern as parse-worker.ts).
// Keyed by tree-sitter node reference — cleared at the start of each file.
const classIdCache = new Map<any, string | null>();
const exportCache = new Map<any, boolean>();
const classIdCache = new Map<SyntaxNode, string | null>();
const exportCache = new Map<SyntaxNode, boolean>();
const cachedFindEnclosingClassId = (node: any, filePath: string): string | null => {
const cachedFindEnclosingClassId = (node: SyntaxNode, filePath: string): string | null => {
const cached = classIdCache.get(node);
if (cached !== undefined) return cached;
const result = findEnclosingClassId(node, filePath);
@ -196,8 +196,8 @@ const cachedFindEnclosingClassId = (node: any, filePath: string): string | null
};
const cachedExportCheck = (
checker: (node: any, name: string) => boolean,
node: any,
checker: (node: SyntaxNode, name: string) => boolean,
node: SyntaxNode,
name: string,
): boolean => {
const cached = exportCache.get(node);
@ -210,7 +210,7 @@ const cachedExportCheck = (
// FieldExtractor cache for sequential path — same pattern as parse-worker.ts
const seqFieldInfoCache = new Map<number, Map<string, FieldInfo>>();
function seqFindEnclosingClassNode(node: any): any | null {
function seqFindEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
let current = node.parent;
while (current) {
if (CLASS_CONTAINER_TYPES.has(current.type)) return current;
@ -221,11 +221,11 @@ function seqFindEnclosingClassNode(node: any): any | null {
/** Minimal no-op SymbolTable stub for FieldExtractorContext (sequential path has a real
* SymbolTable, but it's incomplete at this stage — use the stub for safety). */
const NOOP_SYMBOL_TABLE_SEQ: any = {
const NOOP_SYMBOL_TABLE_SEQ = {
lookupExactAll: () => [],
lookupExact: () => undefined,
lookupExactFull: () => undefined,
};
} as unknown as SymbolTable;
function seqGetFieldInfo(
classNode: SyntaxNode,
@ -321,7 +321,7 @@ const processParsingSequential = async (
: null;
matches.forEach((match) => {
const captureMap: Record<string, any> = {};
const captureMap: Record<string, SyntaxNode> = {};
match.captures.forEach((c) => {
captureMap[c.name] = c.node;
@ -372,7 +372,7 @@ const processParsingSequential = async (
const node: GraphNode = {
id: nodeId,
label: nodeLabel as any,
label: nodeLabel as NodeLabel,
properties: {
name: nodeName,
filePath: file.path,

View file

@ -33,9 +33,11 @@ const SMART_PTR_WRAPPERS = new Set(['shared_ptr', 'unique_ptr', 'weak_ptr']);
* Unwraps type_descriptor wrappers common in tree-sitter-cpp ASTs.
* Returns undefined if no template arguments or no type found. */
export const extractFirstTemplateTypeArg = (parentNode: SyntaxNode): string | undefined => {
const templateArgs = parentNode.children.find((c: any) => c.type === 'template_argument_list');
const templateArgs = parentNode.children.find(
(c: SyntaxNode) => c.type === 'template_argument_list',
);
if (!templateArgs?.firstNamedChild) return undefined;
let argNode: any = templateArgs.firstNamedChild;
let argNode: SyntaxNode | null = templateArgs.firstNamedChild;
if (argNode.type === 'type_descriptor') {
const inner = argNode.childForFieldName('type');
if (inner) argNode = inner;
@ -141,7 +143,7 @@ const extractInitializer: InitializerExtractor = (
func.type === 'template_function'
? func
: func.type === 'qualified_identifier' || func.type === 'scoped_identifier'
? (func.namedChildren.find((c: any) => c.type === 'template_function') ?? null)
? (func.namedChildren.find((c: SyntaxNode) => c.type === 'template_function') ?? null)
: null;
if (templateFunc) {
const nameNode = templateFunc.firstNamedChild;

View file

@ -38,7 +38,7 @@ export const DEFINITION_CAPTURE_KEYS = [
/** Extract the definition node from a tree-sitter query capture map. */
export const getDefinitionNodeFromCaptures = (
captureMap: Record<string, any>,
captureMap: Record<string, SyntaxNode>,
): SyntaxNode | null => {
for (const key of DEFINITION_CAPTURE_KEYS) {
if (captureMap[key]) return captureMap[key];
@ -149,7 +149,9 @@ export const CONTAINER_TYPE_TO_LABEL: Record<string, string> = {
/** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
* Kotlin grammar uses function_declaration for both top-level functions and class methods.
* Returns true when the captured definition node has a class_body ancestor. */
export function isKotlinClassMethod(captureNode: { parent?: any } | null | undefined): boolean {
export function isKotlinClassMethod(
captureNode: { parent?: SyntaxNode | null } | null | undefined,
): boolean {
let ancestor = captureNode?.parent;
while (ancestor) {
if (ancestor.type === 'class_body') return true;
@ -165,7 +167,7 @@ export function isKotlinClassMethod(captureNode: { parent?: any } | null | undef
* Returns null if the capture should be skipped (import, call, C/C++ duplicate, missing name).
*/
export function getLabelFromCaptures(
captureMap: Record<string, any>,
captureMap: Record<string, SyntaxNode>,
provider: LanguageProvider,
): NodeLabel | null {
if (captureMap['import'] || captureMap['call']) return null;
@ -204,7 +206,7 @@ export function getLabelFromCaptures(
/** Walk up AST to find enclosing class/struct/interface/impl, return its generateId or null.
* For Go method_declaration nodes, extracts receiver type (e.g. `func (u *User) Save()` → User struct). */
export const findEnclosingClassId = (node: any, filePath: string): string | null => {
export const findEnclosingClassId = (node: SyntaxNode, filePath: string): string | null => {
let current = node.parent;
while (current) {
// Go: method_declaration has a receiver parameter with the struct type
@ -213,7 +215,7 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
if (receiver) {
// receiver is a parameter_list: (u *User) or (u User)
const paramDecl = receiver.namedChildren?.find?.(
(c: any) => c.type === 'parameter_declaration',
(c: SyntaxNode) => c.type === 'parameter_declaration',
);
if (paramDecl) {
const typeNode = paramDecl.childForFieldName?.('type');
@ -230,7 +232,7 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
// Go: type_declaration wrapping a struct_type (type User struct { ... })
// field_declaration → field_declaration_list → struct_type → type_spec → type_declaration
if (current.type === 'type_declaration') {
const typeSpec = current.children?.find((c: any) => c.type === 'type_spec');
const typeSpec = current.children?.find((c: SyntaxNode) => c.type === 'type_spec');
if (typeSpec) {
const typeBody = typeSpec.childForFieldName?.('type');
if (typeBody?.type === 'struct_type' || typeBody?.type === 'interface_type') {
@ -246,11 +248,11 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
if (current.type === 'impl_item') {
const children = current.children ?? [];
const forIdx = children.findIndex((c: any) => c.text === 'for');
const forIdx = children.findIndex((c: SyntaxNode) => c.text === 'for');
if (forIdx !== -1) {
const nameNode = children
.slice(forIdx + 1)
.find((c: any) => c.type === 'type_identifier' || c.type === 'identifier');
.find((c: SyntaxNode) => c.type === 'type_identifier' || c.type === 'identifier');
if (nameNode) {
return generateId('Impl', `${filePath}:${nameNode.text}`);
}
@ -260,7 +262,7 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
const nameNode =
current.childForFieldName?.('name') ??
current.children?.find(
(c: any) =>
(c: SyntaxNode) =>
c.type === 'type_identifier' ||
c.type === 'identifier' ||
c.type === 'name' ||
@ -281,10 +283,10 @@ export const findEnclosingClassId = (node: any, filePath: string): string | null
* Used for Kotlin AST traversal where visibility_modifier lives inside a modifiers sibling.
*/
export const findSiblingChild = (
parent: any,
parent: SyntaxNode,
siblingType: string,
childType: string,
): any | null => {
): SyntaxNode | null => {
for (let i = 0; i < parent.childCount; i++) {
const sibling = parent.child(i);
if (sibling?.type === siblingType) {
@ -843,7 +845,7 @@ export const extractMethodSignature = (node: SyntaxNode | null | undefined): Met
// ============================================================================
/** Walk an AST node depth-first, returning the first descendant with the given type. */
export function findDescendant(node: any, type: string): any {
export function findDescendant(node: SyntaxNode, type: string): SyntaxNode | null {
if (node.type === type) return node;
for (const child of node.children ?? []) {
const found = findDescendant(child, type);
@ -853,9 +855,9 @@ export function findDescendant(node: any, type: string): any {
}
/** Extract the text content from a string or encapsed_string AST node. */
export function extractStringContent(node: any): string | null {
export function extractStringContent(node: SyntaxNode | null | undefined): string | null {
if (!node) return null;
const content = node.children?.find((c: any) => c.type === 'string_content');
const content = node.children?.find((c: SyntaxNode) => c.type === 'string_content');
if (content) return content.text;
if (node.type === 'string_content') return node.text;
return null;

View file

@ -15,22 +15,26 @@ import { createRequire } from 'node:module';
import { SupportedLanguages } from 'gitnexus-shared';
import { getProvider } from '../languages/index.js';
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from '../constants.js';
import { SymbolTable } from '../symbol-table.js';
/** Language grammar type accepted by Parser.setLanguage(). */
type TreeSitterLanguage = Parameters<typeof Parser.prototype.setLanguage>[0];
// tree-sitter-swift is an optionalDependency — may not be installed
const _require = createRequire(import.meta.url);
let Swift: any = null;
let Swift: TreeSitterLanguage | null = null;
try {
Swift = _require('tree-sitter-swift');
} catch {}
// tree-sitter-dart is an optionalDependency — may not be installed
let Dart: any = null;
let Dart: TreeSitterLanguage | null = null;
try {
Dart = _require('tree-sitter-dart');
} catch {}
// tree-sitter-kotlin is an optionalDependency — may not be installed
let Kotlin: any = null;
let Kotlin: TreeSitterLanguage | null = null;
try {
Kotlin = _require('tree-sitter-kotlin');
} catch {}
@ -250,13 +254,18 @@ export interface ParseWorkerInput {
content: string;
}
type WorkerIncomingMessage =
| { type: 'sub-batch'; files: ParseWorkerInput[] }
| { type: 'flush' }
| ParseWorkerInput[];
// ============================================================================
// Worker-local parser + language map
// ============================================================================
const parser = new Parser();
const languageMap: Record<string, any> = {
const languageMap: Record<string, TreeSitterLanguage> = {
[SupportedLanguages.JavaScript]: JavaScript,
[SupportedLanguages.TypeScript]: TypeScript.typescript,
[`${SupportedLanguages.TypeScript}:tsx`]: TypeScript.tsx,
@ -305,9 +314,9 @@ const setLanguage = (language: SupportedLanguages, filePath: string): void => {
// a stored null (enclosing class/function not found = top-level).
// ============================================================================
const classIdCache = new Map<any, string | null>();
const functionIdCache = new Map<any, string | null>();
const exportCache = new Map<any, boolean>();
const classIdCache = new Map<SyntaxNode, string | null>();
const functionIdCache = new Map<SyntaxNode, string | null>();
const exportCache = new Map<SyntaxNode, boolean>();
const clearCaches = (): void => {
classIdCache.clear();
@ -343,11 +352,11 @@ function findEnclosingClassNode(node: SyntaxNode): SyntaxNode | null {
* Field extraction only uses symbolTable.lookupExactAll for optional type resolution —
* returning [] causes the extractor to use the raw type string, which is fine for us.
*/
const NOOP_SYMBOL_TABLE: any = {
const NOOP_SYMBOL_TABLE = {
lookupExactAll: () => [],
lookupExact: () => undefined,
lookupExactFull: () => undefined,
};
} as unknown as SymbolTable;
/**
* Get (or extract and cache) field info for a class node.
@ -385,7 +394,7 @@ import type { LanguageProvider } from '../language-provider.js';
/** Walk up AST to find enclosing function, return its generateId or null for top-level.
* Applies provider.labelOverride so the label matches the definition phase (single source of truth). */
const findEnclosingFunctionId = (
node: any,
node: SyntaxNode,
filePath: string,
provider: LanguageProvider,
): string | null => {
@ -433,7 +442,7 @@ const findEnclosingFunctionId = (
};
/** Cached wrapper for findEnclosingClassId — avoids repeated parent walks. */
const cachedFindEnclosingClassId = (node: any, filePath: string): string | null => {
const cachedFindEnclosingClassId = (node: SyntaxNode, filePath: string): string | null => {
const cached = classIdCache.get(node);
if (cached !== undefined) return cached;
@ -444,8 +453,8 @@ const cachedFindEnclosingClassId = (node: any, filePath: string): string | null
/** Cached wrapper for export checking — avoids repeated parent walks per symbol. */
const cachedExportCheck = (
checker: (node: any, name: string) => boolean,
node: any,
checker: (node: SyntaxNode, name: string) => boolean,
node: SyntaxNode,
name: string,
): boolean => {
const cached = exportCache.get(node);
@ -635,26 +644,26 @@ const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update',
const API_RESOURCE_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
/** Check if node is a scoped_call_expression with object 'Route' */
function isRouteStaticCall(node: any): boolean {
function isRouteStaticCall(node: SyntaxNode): boolean {
if (node.type !== 'scoped_call_expression') return false;
const obj = node.childForFieldName?.('object') ?? node.children?.[0];
return obj?.text === 'Route';
}
/** Get the method name from a scoped_call_expression or member_call_expression */
function getCallMethodName(node: any): string | null {
function getCallMethodName(node: SyntaxNode): string | null {
const nameNode =
node.childForFieldName?.('name') ?? node.children?.find((c: any) => c.type === 'name');
node.childForFieldName?.('name') ?? node.children?.find((c: SyntaxNode) => c.type === 'name');
return nameNode?.text ?? null;
}
/** Get the arguments node from a call expression */
function getArguments(node: any): any {
return node.children?.find((c: any) => c.type === 'arguments') ?? null;
function getArguments(node: SyntaxNode): SyntaxNode | null {
return node.children?.find((c: SyntaxNode) => c.type === 'arguments') ?? null;
}
/** Find the closure body inside arguments */
function findClosureBody(argsNode: any): any | null {
function findClosureBody(argsNode: SyntaxNode | null): SyntaxNode | null {
if (!argsNode) return null;
for (const child of argsNode.children ?? []) {
if (child.type === 'argument') {
@ -662,7 +671,8 @@ function findClosureBody(argsNode: any): any | null {
if (inner.type === 'anonymous_function' || inner.type === 'arrow_function') {
return (
inner.childForFieldName?.('body') ??
inner.children?.find((c: any) => c.type === 'compound_statement')
inner.children?.find((c: SyntaxNode) => c.type === 'compound_statement') ??
null
);
}
}
@ -670,7 +680,8 @@ function findClosureBody(argsNode: any): any | null {
if (child.type === 'anonymous_function' || child.type === 'arrow_function') {
return (
child.childForFieldName?.('body') ??
child.children?.find((c: any) => c.type === 'compound_statement')
child.children?.find((c: SyntaxNode) => c.type === 'compound_statement') ??
null
);
}
}
@ -678,7 +689,7 @@ function findClosureBody(argsNode: any): any | null {
}
/** Extract first string argument from arguments node */
function extractFirstStringArg(argsNode: any): string | null {
function extractFirstStringArg(argsNode: SyntaxNode | null): string | null {
if (!argsNode) return null;
for (const child of argsNode.children ?? []) {
const target = child.type === 'argument' ? child.children?.[0] : child;
@ -691,7 +702,7 @@ function extractFirstStringArg(argsNode: any): string | null {
}
/** Extract middleware from arguments — handles string or array */
function extractMiddlewareArg(argsNode: any): string[] {
function extractMiddlewareArg(argsNode: SyntaxNode | null): string[] {
if (!argsNode) return [];
for (const child of argsNode.children ?? []) {
const target = child.type === 'argument' ? child.children?.[0] : child;
@ -705,7 +716,7 @@ function extractMiddlewareArg(argsNode: any): string[] {
for (const el of target.children ?? []) {
if (el.type === 'array_element_initializer') {
const str = el.children?.find(
(c: any) => c.type === 'string' || c.type === 'encapsed_string',
(c: SyntaxNode) => c.type === 'string' || c.type === 'encapsed_string',
);
const val = str ? extractStringContent(str) : null;
if (val) items.push(val);
@ -718,25 +729,25 @@ function extractMiddlewareArg(argsNode: any): string[] {
}
/** Extract Controller::class from arguments */
function extractClassArg(argsNode: any): string | null {
function extractClassArg(argsNode: SyntaxNode | null): string | null {
if (!argsNode) return null;
for (const child of argsNode.children ?? []) {
const target = child.type === 'argument' ? child.children?.[0] : child;
if (target?.type === 'class_constant_access_expression') {
return target.children?.find((c: any) => c.type === 'name')?.text ?? null;
return target.children?.find((c: SyntaxNode) => c.type === 'name')?.text ?? null;
}
}
return null;
}
/** Extract controller class name from arguments: [Controller::class, 'method'] or 'Controller@method' */
function extractControllerTarget(argsNode: any): {
function extractControllerTarget(argsNode: SyntaxNode | null): {
controller: string | null;
method: string | null;
} {
if (!argsNode) return { controller: null, method: null };
const args: any[] = [];
const args: (SyntaxNode | undefined)[] = [];
for (const child of argsNode.children ?? []) {
if (child.type === 'argument') args.push(child.children?.[0]);
else if (child.type !== '(' && child.type !== ')' && child.type !== ',') args.push(child);
@ -750,14 +761,14 @@ function extractControllerTarget(argsNode: any): {
if (handlerNode.type === 'array_creation_expression') {
let controller: string | null = null;
let method: string | null = null;
const elements: any[] = [];
const elements: SyntaxNode[] = [];
for (const el of handlerNode.children ?? []) {
if (el.type === 'array_element_initializer') elements.push(el);
}
if (elements[0]) {
const classAccess = findDescendant(elements[0], 'class_constant_access_expression');
if (classAccess) {
controller = classAccess.children?.find((c: any) => c.type === 'name')?.text ?? null;
controller = classAccess.children?.find((c: SyntaxNode) => c.type === 'name')?.text ?? null;
}
}
if (elements[1]) {
@ -778,7 +789,8 @@ function extractControllerTarget(argsNode: any): {
// Class reference: UserController::class (invokable controller)
if (handlerNode.type === 'class_constant_access_expression') {
const controller = handlerNode.children?.find((c: any) => c.type === 'name')?.text ?? null;
const controller =
handlerNode.children?.find((c: SyntaxNode) => c.type === 'name')?.text ?? null;
return { controller, method: '__invoke' };
}
@ -788,22 +800,22 @@ function extractControllerTarget(argsNode: any): {
interface ChainedRouteCall {
isRouteFacade: boolean;
terminalMethod: string;
attributes: { method: string; argsNode: any }[];
terminalArgs: any;
node: any;
attributes: { method: string; argsNode: SyntaxNode | null }[];
terminalArgs: SyntaxNode | null;
node: SyntaxNode;
}
/**
* Unwrap a chained call like Route::middleware('auth')->prefix('api')->group(fn)
*/
function unwrapRouteChain(node: any): ChainedRouteCall | null {
function unwrapRouteChain(node: SyntaxNode): ChainedRouteCall | null {
if (node.type !== 'member_call_expression') return null;
const terminalMethod = getCallMethodName(node);
if (!terminalMethod) return null;
const terminalArgs = getArguments(node);
const attributes: { method: string; argsNode: any }[] = [];
const attributes: { method: string; argsNode: SyntaxNode | null }[] = [];
let current = node.children?.[0];
@ -831,7 +843,7 @@ function unwrapRouteChain(node: any): ChainedRouteCall | null {
}
/** Parse Route::group(['middleware' => ..., 'prefix' => ...], fn) array syntax */
function parseArrayGroupArgs(argsNode: any): RouteGroupContext {
function parseArrayGroupArgs(argsNode: SyntaxNode | null): RouteGroupContext {
const ctx: RouteGroupContext = { middleware: [], prefix: null, controller: null };
if (!argsNode) return ctx;
@ -841,7 +853,7 @@ function parseArrayGroupArgs(argsNode: any): RouteGroupContext {
for (const el of target.children ?? []) {
if (el.type !== 'array_element_initializer') continue;
const children = el.children ?? [];
const arrowIdx = children.findIndex((c: any) => c.type === '=>');
const arrowIdx = children.findIndex((c: SyntaxNode) => c.type === '=>');
if (arrowIdx === -1) continue;
const key = extractStringContent(children[arrowIdx - 1]);
const val = children[arrowIdx + 1];
@ -852,7 +864,7 @@ function parseArrayGroupArgs(argsNode: any): RouteGroupContext {
} else if (val?.type === 'array_creation_expression') {
for (const item of val.children ?? []) {
if (item.type === 'array_element_initializer') {
const str = item.children?.find((c: any) => c.type === 'string');
const str = item.children?.find((c: SyntaxNode) => c.type === 'string');
const s = str ? extractStringContent(str) : null;
if (s) ctx.middleware.push(s);
}
@ -862,7 +874,7 @@ function parseArrayGroupArgs(argsNode: any): RouteGroupContext {
ctx.prefix = extractStringContent(val) ?? null;
} else if (key === 'controller') {
if (val?.type === 'class_constant_access_expression') {
ctx.controller = val.children?.find((c: any) => c.type === 'name')?.text ?? null;
ctx.controller = val.children?.find((c: SyntaxNode) => c.type === 'name')?.text ?? null;
}
}
}
@ -871,7 +883,7 @@ function parseArrayGroupArgs(argsNode: any): RouteGroupContext {
return ctx;
}
function extractLaravelRoutes(tree: any, filePath: string): ExtractedRoute[] {
function extractLaravelRoutes(tree: Parser.Tree, filePath: string): ExtractedRoute[] {
const routes: ExtractedRoute[] = [];
function resolveStack(stack: RouteGroupContext[]): {
@ -892,10 +904,10 @@ function extractLaravelRoutes(tree: any, filePath: string): ExtractedRoute[] {
function emitRoute(
httpMethod: string,
argsNode: any,
argsNode: SyntaxNode | null,
lineNumber: number,
groupStack: RouteGroupContext[],
chainAttrs: { method: string; argsNode: any }[],
chainAttrs: { method: string; argsNode: SyntaxNode | null }[],
) {
const effective = resolveStack(groupStack);
@ -944,7 +956,7 @@ function extractLaravelRoutes(tree: any, filePath: string): ExtractedRoute[] {
}
}
function walk(node: any, groupStack: RouteGroupContext[]) {
function walk(node: SyntaxNode, groupStack: RouteGroupContext[]) {
// Case 1: Simple Route::get(...), Route::post(...), etc.
if (isRouteStaticCall(node)) {
const method = getCallMethodName(node);
@ -1003,7 +1015,7 @@ function extractLaravelRoutes(tree: any, filePath: string): ExtractedRoute[] {
walkChildren(node, groupStack);
}
function walkChildren(node: any, groupStack: RouteGroupContext[]) {
function walkChildren(node: SyntaxNode, groupStack: RouteGroupContext[]) {
for (const child of node.children ?? []) {
walk(child, groupStack);
}
@ -1073,7 +1085,7 @@ const processFileGroup = (
result: ParseWorkerResult,
onFileProcessed?: () => void,
): void => {
let query: any;
let query: Parser.Query;
try {
const lang = parser.getLanguage();
query = new Parser.Query(lang, queryString);
@ -1124,7 +1136,7 @@ const processFileGroup = (
// runs. This pre-pass makes parent class information available for type resolution.
const fileParentMap = new Map<string, string[]>();
for (const match of matches) {
const captureMap: Record<string, any> = {};
const captureMap: Record<string, SyntaxNode> = {};
for (const c of match.captures) {
captureMap[c.name] = c.node;
}
@ -1176,7 +1188,7 @@ const processFileGroup = (
const fileDecorators = new Map<number, { name: string; arg?: string; isTool?: boolean }>();
for (const match of matches) {
const captureMap: Record<string, any> = {};
const captureMap: Record<string, SyntaxNode> = {};
for (const c of match.captures) {
captureMap[c.name] = c.node;
}
@ -1830,10 +1842,19 @@ const mergeResult = (target: ParseWorkerResult, src: ParseWorkerResult) => {
target.fileCount += src.fileCount;
};
parentPort!.on('message', (msg: any) => {
parentPort!.on('message', (msg: WorkerIncomingMessage) => {
try {
// Legacy single-message mode (backward compat): array of files
if (Array.isArray(msg)) {
const result = processBatch(msg, (filesProcessed) => {
parentPort!.postMessage({ type: 'progress', filesProcessed });
});
parentPort!.postMessage({ type: 'result', data: result });
return;
}
// Sub-batch mode: { type: 'sub-batch', files: [...] }
if (msg && msg.type === 'sub-batch') {
if (msg.type === 'sub-batch') {
const result = processBatch(msg.files, (filesProcessed) => {
parentPort!.postMessage({
type: 'progress',
@ -1848,7 +1869,7 @@ parentPort!.on('message', (msg: any) => {
}
// Flush: send accumulated results
if (msg && msg.type === 'flush') {
if (msg.type === 'flush') {
parentPort!.postMessage({ type: 'result', data: accumulated });
// Reset for potential reuse
accumulated = {
@ -1872,15 +1893,6 @@ parentPort!.on('message', (msg: any) => {
cumulativeProcessed = 0;
return;
}
// Legacy single-message mode (backward compat): array of files
if (Array.isArray(msg)) {
const result = processBatch(msg, (filesProcessed) => {
parentPort!.postMessage({ type: 'progress', filesProcessed });
});
parentPort!.postMessage({ type: 'result', data: result });
return;
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
parentPort!.postMessage({ type: 'error', error: message });

View file

@ -21,6 +21,13 @@ export interface WorkerPool {
readonly size: number;
}
/** Message shapes sent back by worker threads. */
type WorkerOutgoingMessage =
| { type: 'progress'; filesProcessed: number }
| { type: 'sub-batch-done' }
| { type: 'error'; error: string }
| { type: 'result'; data: unknown };
/**
* Max files to send to a worker in a single postMessage.
* Keeps structured-clone memory bounded per sub-batch.
@ -105,32 +112,28 @@ export const createWorkerPool = (workerUrl: URL, poolSize?: number): WorkerPool
worker.postMessage({ type: 'sub-batch', files: subBatch });
};
const handler = (msg: any) => {
const handler = (msg: WorkerOutgoingMessage) => {
if (settled) return;
if (msg && msg.type === 'progress') {
if (msg.type === 'progress') {
workerProgress[i] = msg.filesProcessed;
if (onProgress) {
const total = workerProgress.reduce((a, b) => a + b, 0);
onProgress(total);
}
} else if (msg && msg.type === 'sub-batch-done') {
} else if (msg.type === 'sub-batch-done') {
sendNextSubBatch();
} else if (msg && msg.type === 'error') {
} else if (msg.type === 'error') {
settled = true;
cleanup();
reject(new Error(`Worker ${i} error: ${msg.error}`));
} else if (msg && msg.type === 'result') {
} else if (msg.type === 'result') {
settled = true;
cleanup();
resolve(msg.data);
} else {
settled = true;
cleanup();
resolve(msg);
resolve(msg.data as TResult);
}
};
const errorHandler = (err: any) => {
const errorHandler = (err: Error) => {
if (!settled) {
settled = true;
cleanup();

View file

@ -344,7 +344,7 @@ export const streamAllCSVsToDisk = async (
);
break;
case 'Community': {
const keywords = (node.properties as any).keywords || [];
const keywords = node.properties.keywords || [];
const keywordsStr = `[${keywords.map((k: string) => `'${k.replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/,/g, '\\,')}'`).join(',')}]`;
await communityWriter.addRow(
[
@ -352,8 +352,8 @@ export const streamAllCSVsToDisk = async (
escapeCSVField(node.properties.name || ''),
escapeCSVField(node.properties.heuristicLabel || ''),
keywordsStr,
escapeCSVField((node.properties as any).description || ''),
escapeCSVField((node.properties as any).enrichedBy || 'heuristic'),
escapeCSVField(node.properties.description || ''),
escapeCSVField(node.properties.enrichedBy || 'heuristic'),
escapeCSVNumber(node.properties.cohesion, 0),
escapeCSVNumber(node.properties.symbolCount, 0),
].join(','),
@ -361,18 +361,18 @@ export const streamAllCSVsToDisk = async (
break;
}
case 'Process': {
const communities = (node.properties as any).communities || [];
const communities = node.properties.communities || [];
const communitiesStr = `[${communities.map((c: string) => `'${c.replace(/'/g, "''")}'`).join(',')}]`;
await processWriter.addRow(
[
escapeCSVField(node.id),
escapeCSVField(node.properties.name || ''),
escapeCSVField((node.properties as any).heuristicLabel || ''),
escapeCSVField((node.properties as any).processType || ''),
escapeCSVNumber((node.properties as any).stepCount, 0),
escapeCSVField(node.properties.heuristicLabel || ''),
escapeCSVField(node.properties.processType || ''),
escapeCSVNumber(node.properties.stepCount, 0),
escapeCSVField(communitiesStr),
escapeCSVField((node.properties as any).entryPointId || ''),
escapeCSVField((node.properties as any).terminalId || ''),
escapeCSVField(node.properties.entryPointId || ''),
escapeCSVField(node.properties.terminalId || ''),
].join(','),
);
break;
@ -388,7 +388,7 @@ export const streamAllCSVsToDisk = async (
escapeCSVNumber(node.properties.endLine, -1),
node.properties.isExported ? 'true' : 'false',
escapeCSVField(content),
escapeCSVField((node.properties as any).description || ''),
escapeCSVField(node.properties.description || ''),
escapeCSVNumber(node.properties.parameterCount, 0),
escapeCSVField(node.properties.returnType || ''),
].join(','),
@ -404,21 +404,21 @@ export const streamAllCSVsToDisk = async (
escapeCSVField(node.properties.filePath || ''),
escapeCSVNumber(node.properties.startLine, -1),
escapeCSVNumber(node.properties.endLine, -1),
escapeCSVNumber((node.properties as any).level, 1),
escapeCSVNumber(node.properties.level, 1),
escapeCSVField(content),
escapeCSVField((node.properties as any).description || ''),
escapeCSVField(node.properties.description || ''),
].join(','),
);
break;
}
case 'Route': {
const responseKeys = (node.properties as any).responseKeys || [];
const responseKeys = node.properties.responseKeys || [];
// LadybugDB array literal inside a quoted CSV field: escapeCSVField wraps in "..."
// and the array uses single-quoted elements
const keysStr = `[${responseKeys.map((k: string) => `'${k.replace(/'/g, "''")}'`).join(',')}]`;
const errorKeys = (node.properties as any).errorKeys || [];
const errorKeys = node.properties.errorKeys || [];
const errorKeysStr = `[${errorKeys.map((k: string) => `'${k.replace(/'/g, "''")}'`).join(',')}]`;
const middleware = (node.properties as any).middleware || [];
const middleware = node.properties.middleware || [];
const middlewareStr = `[${middleware.map((m: string) => `'${m.replace(/'/g, "''")}'`).join(',')}]`;
await routeWriter.addRow(
[
@ -438,7 +438,7 @@ export const streamAllCSVsToDisk = async (
escapeCSVField(node.id),
escapeCSVField(node.properties.name || ''),
escapeCSVField(node.properties.filePath || ''),
escapeCSVField((node.properties as any).description || ''),
escapeCSVField(node.properties.description || ''),
].join(','),
);
break;
@ -456,7 +456,7 @@ export const streamAllCSVsToDisk = async (
escapeCSVNumber(node.properties.endLine, -1),
node.properties.isExported ? 'true' : 'false',
escapeCSVField(content),
escapeCSVField((node.properties as any).description || ''),
escapeCSVField(node.properties.description || ''),
].join(','),
);
} else {
@ -472,7 +472,7 @@ export const streamAllCSVsToDisk = async (
escapeCSVNumber(node.properties.startLine, -1),
escapeCSVNumber(node.properties.endLine, -1),
escapeCSVField(content),
escapeCSVField((node.properties as any).description || ''),
escapeCSVField(node.properties.description || ''),
].join(','),
);
}