From 2ca7df642b01352844ce1eb8e208f197247fe349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Magyar?= Date: Sat, 28 Mar 2026 17:11:24 +0000 Subject: [PATCH] refactor: reduce explicit any types (#566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- gitnexus-shared/src/graph/types.ts | 2 +- gitnexus/src/core/ingestion/ast-cache.ts | 2 +- gitnexus/src/core/ingestion/call-routing.ts | 4 +- .../src/core/ingestion/community-processor.ts | 38 +++-- .../src/core/ingestion/import-processor.ts | 2 +- gitnexus/src/core/ingestion/languages/php.ts | 27 ++-- .../src/core/ingestion/parsing-processor.ts | 24 ++-- .../core/ingestion/type-extractors/c-cpp.ts | 8 +- .../src/core/ingestion/utils/ast-helpers.ts | 30 ++-- .../core/ingestion/workers/parse-worker.ts | 132 ++++++++++-------- .../src/core/ingestion/workers/worker-pool.ts | 25 ++-- gitnexus/src/core/lbug/csv-generator.ts | 36 ++--- 12 files changed, 185 insertions(+), 145 deletions(-) diff --git a/gitnexus-shared/src/graph/types.ts b/gitnexus-shared/src/graph/types.ts index e89b42117e..9ab2733ed3 100644 --- a/gitnexus-shared/src/graph/types.ts +++ b/gitnexus-shared/src/graph/types.ts @@ -83,7 +83,7 @@ export type NodeProperties = { errorKeys?: string[]; middleware?: string[]; // Extensible - [key: string]: any; + [key: string]: unknown; }; export type RelationshipType = diff --git a/gitnexus/src/core/ingestion/ast-cache.ts b/gitnexus/src/core/ingestion/ast-cache.ts index ee971023e3..2dc637f917 100644 --- a/gitnexus/src/core/ingestion/ast-cache.ts +++ b/gitnexus/src/core/ingestion/ast-cache.ts @@ -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); } diff --git a/gitnexus/src/core/ingestion/call-routing.ts b/gitnexus/src/core/ingestion/call-routing.ts index 2201409efb..3922c0c89d 100644 --- a/gitnexus/src/core/ingestion/call-routing.ts +++ b/gitnexus/src/core/ingestion/call-routing.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/community-processor.ts b/gitnexus/src/core/ingestion/community-processor.ts index ab0b1bd6aa..9913e4a3f1 100644 --- a/gitnexus/src/core/ingestion/community-processor.ts +++ b/gitnexus/src/core/ingestion/community-processor.ts @@ -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; + +const leiden: LeidenModule = _require(leidenPath); + +/** Vendored Leiden algorithm module shape */ +interface LeidenModule { + detailed: (graph: GraphInstance, options: Record) => LeidenDetailedResult; +} + +/** Result returned by leiden.detailed() */ +interface LeidenDetailedResult { + communities: Record; + 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((_, 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(['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, 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, - 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); diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 27e08f4548..3c52fa7a6a 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -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; } diff --git a/gitnexus/src/core/ingestion/languages/php.ts b/gitnexus/src/core/ingestion/languages/php.ts index 1ac4164e51..d11a51df8c 100644 --- a/gitnexus/src/core/ingestion/languages/php.ts +++ b/gitnexus/src/core/ingestion/languages/php.ts @@ -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, + captureMap: Record, ): string | undefined { if (nodeLabel === 'Property' && captureMap['definition.property']) { return extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined; diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index e6e37e6035..242909064f 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -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(); -const exportCache = new Map(); +const classIdCache = new Map(); +const exportCache = new Map(); -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>(); -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 = {}; + const captureMap: Record = {}; 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, diff --git a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts index 4bc6e45462..0544d4f9da 100644 --- a/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts +++ b/gitnexus/src/core/ingestion/type-extractors/c-cpp.ts @@ -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; diff --git a/gitnexus/src/core/ingestion/utils/ast-helpers.ts b/gitnexus/src/core/ingestion/utils/ast-helpers.ts index a4120869bb..d5b699f48e 100644 --- a/gitnexus/src/core/ingestion/utils/ast-helpers.ts +++ b/gitnexus/src/core/ingestion/utils/ast-helpers.ts @@ -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, + captureMap: Record, ): 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 = { /** 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, + captureMap: Record, 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; diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 7bfef15bf2..c374346071 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -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[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 = { +const languageMap: Record = { [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(); -const functionIdCache = new Map(); -const exportCache = new Map(); +const classIdCache = new Map(); +const functionIdCache = new Map(); +const exportCache = new Map(); 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(); for (const match of matches) { - const captureMap: Record = {}; + const captureMap: Record = {}; for (const c of match.captures) { captureMap[c.name] = c.node; } @@ -1176,7 +1188,7 @@ const processFileGroup = ( const fileDecorators = new Map(); for (const match of matches) { - const captureMap: Record = {}; + const captureMap: Record = {}; 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 }); diff --git a/gitnexus/src/core/ingestion/workers/worker-pool.ts b/gitnexus/src/core/ingestion/workers/worker-pool.ts index 4216c372cf..bc23f0ea41 100644 --- a/gitnexus/src/core/ingestion/workers/worker-pool.ts +++ b/gitnexus/src/core/ingestion/workers/worker-pool.ts @@ -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(); diff --git a/gitnexus/src/core/lbug/csv-generator.ts b/gitnexus/src/core/lbug/csv-generator.ts index 06789619e0..b3a53146ea 100644 --- a/gitnexus/src/core/lbug/csv-generator.ts +++ b/gitnexus/src/core/lbug/csv-generator.ts @@ -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(','), ); }