Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12ee6b100b | ||
|
|
3d4a787ad4 | ||
|
|
23d3765a8d | ||
|
|
824b22ba1a | ||
|
|
1d9a9c476d |
@@ -327,7 +327,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas
|
||||
|
||||
### Supported Languages
|
||||
|
||||
TypeScript, JavaScript, Python, Java, Kotlin, C, C++, C#, Go, Ruby, Rust, PHP, Swift
|
||||
TypeScript, JavaScript, Python, Java, Kotlin, C, C++, C#, Go, Rust, PHP, Swift
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
@@ -5,42 +5,6 @@ import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { useAppState } from '../hooks/useAppState';
|
||||
import { NODE_COLORS } from '../lib/constants';
|
||||
|
||||
/** Map file extension to Prism syntax highlighter language identifier */
|
||||
const getSyntaxLanguage = (filePath: string | undefined): string => {
|
||||
if (!filePath) return 'text';
|
||||
const ext = filePath.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'js': case 'jsx': case 'mjs': case 'cjs': return 'javascript';
|
||||
case 'ts': case 'tsx': case 'mts': case 'cts': return 'typescript';
|
||||
case 'py': case 'pyw': return 'python';
|
||||
case 'rb': case 'rake': case 'gemspec': return 'ruby';
|
||||
case 'java': return 'java';
|
||||
case 'go': return 'go';
|
||||
case 'rs': return 'rust';
|
||||
case 'c': case 'h': return 'c';
|
||||
case 'cpp': case 'cc': case 'cxx': case 'hpp': case 'hxx': case 'hh': return 'cpp';
|
||||
case 'cs': return 'csharp';
|
||||
case 'php': return 'php';
|
||||
case 'kt': case 'kts': return 'kotlin';
|
||||
case 'swift': return 'swift';
|
||||
case 'json': return 'json';
|
||||
case 'yaml': case 'yml': return 'yaml';
|
||||
case 'md': case 'mdx': return 'markdown';
|
||||
case 'html': case 'htm': case 'erb': return 'markup';
|
||||
case 'css': case 'scss': case 'sass': return 'css';
|
||||
case 'sh': case 'bash': case 'zsh': return 'bash';
|
||||
case 'sql': return 'sql';
|
||||
case 'xml': return 'xml';
|
||||
default: break;
|
||||
}
|
||||
// Handle extensionless Ruby files
|
||||
const basename = filePath.split('/').pop() || '';
|
||||
if (['Rakefile', 'Gemfile', 'Guardfile', 'Vagrantfile', 'Brewfile'].includes(basename)) return 'ruby';
|
||||
if (['Makefile'].includes(basename)) return 'makefile';
|
||||
if (['Dockerfile'].includes(basename)) return 'docker';
|
||||
return 'text';
|
||||
};
|
||||
|
||||
// Match the code theme used elsewhere in the app
|
||||
const customTheme = {
|
||||
...vscDarkPlus,
|
||||
@@ -303,7 +267,12 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
|
||||
<div className="flex-1 min-h-0 overflow-auto scrollbar-thin">
|
||||
{selectedFileContent ? (
|
||||
<SyntaxHighlighter
|
||||
language={getSyntaxLanguage(selectedFilePath)}
|
||||
language={
|
||||
selectedFilePath?.endsWith('.py') ? 'python' :
|
||||
selectedFilePath?.endsWith('.js') || selectedFilePath?.endsWith('.jsx') ? 'javascript' :
|
||||
selectedFilePath?.endsWith('.ts') || selectedFilePath?.endsWith('.tsx') ? 'typescript' :
|
||||
'text'
|
||||
}
|
||||
style={customTheme as any}
|
||||
showLineNumbers
|
||||
startingLineNumber={1}
|
||||
@@ -370,7 +339,11 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
|
||||
const hasRange = typeof ref.startLine === 'number';
|
||||
const startDisplay = hasRange ? (ref.startLine ?? 0) + 1 : undefined;
|
||||
const endDisplay = hasRange ? (ref.endLine ?? ref.startLine ?? 0) + 1 : undefined;
|
||||
const language = getSyntaxLanguage(ref.filePath);
|
||||
const language =
|
||||
ref.filePath.endsWith('.py') ? 'python' :
|
||||
ref.filePath.endsWith('.js') || ref.filePath.endsWith('.jsx') ? 'javascript' :
|
||||
ref.filePath.endsWith('.ts') || ref.filePath.endsWith('.tsx') ? 'typescript' :
|
||||
'text';
|
||||
|
||||
const isGlowing = glowRefId === ref.id;
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@ export enum SupportedLanguages {
|
||||
Go = 'go',
|
||||
Rust = 'rust',
|
||||
PHP = 'php',
|
||||
Ruby = 'ruby',
|
||||
// Ruby = 'ruby',
|
||||
Swift = 'swift',
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import { loadParser, loadLanguage } from '../tree-sitter/parser-loader';
|
||||
import { LANGUAGE_QUERIES } from './tree-sitter-queries';
|
||||
import { generateId } from '../../lib/utils';
|
||||
import { getLanguageFromFilename } from './utils';
|
||||
import { SupportedLanguages } from '../../config/supported-languages';
|
||||
import { routeRubyCall } from './ruby-call-routing';
|
||||
|
||||
/**
|
||||
* Node types that represent function/method definitions across languages.
|
||||
@@ -37,9 +35,6 @@ const FUNCTION_NODE_TYPES = new Set([
|
||||
// Rust
|
||||
'function_item',
|
||||
'impl_item', // Methods inside impl blocks
|
||||
// Ruby
|
||||
'method', // def foo
|
||||
'singleton_method', // def self.foo
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -97,18 +92,6 @@ const findEnclosingFunction = (
|
||||
current.children?.find((c: any) => c.type === 'identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method'; // Treat constructors as methods for process detection
|
||||
} else if (current.type === 'method') {
|
||||
// Ruby instance method: def foo
|
||||
const nameNode = current.childForFieldName?.('name') ||
|
||||
current.children?.find((c: any) => c.type === 'identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Method';
|
||||
} else if (current.type === 'singleton_method') {
|
||||
// Ruby class method: def self.foo
|
||||
const nameNode = current.childForFieldName?.('name') ||
|
||||
current.children?.find((c: any) => c.type === 'identifier');
|
||||
funcName = nameNode?.text;
|
||||
label = 'Function';
|
||||
} else if (current.type === 'arrow_function' || current.type === 'function_expression') {
|
||||
// Arrow/expression: const foo = () => {} - check parent variable declarator
|
||||
const parent = current.parent;
|
||||
@@ -201,61 +184,6 @@ export const processCalls = async (
|
||||
|
||||
const calledName = nameNode.text;
|
||||
|
||||
// Ruby: route special calls to heritage or properties (imports handled by import-processor)
|
||||
if (language === SupportedLanguages.Ruby) {
|
||||
const callNode = captureMap['call'];
|
||||
const routed = routeRubyCall(calledName, callNode);
|
||||
|
||||
switch (routed.kind) {
|
||||
case 'skip':
|
||||
case 'import': // handled by import-processor
|
||||
return;
|
||||
|
||||
case 'heritage':
|
||||
for (const item of routed.items) {
|
||||
const childId = symbolTable.lookupExact(file.path, item.enclosingClass) ||
|
||||
symbolTable.lookupFuzzy(item.enclosingClass)[0]?.nodeId ||
|
||||
generateId('Class', `${file.path}:${item.enclosingClass}`);
|
||||
const parentId = symbolTable.lookupFuzzy(item.mixinName)[0]?.nodeId ||
|
||||
generateId('Module', `${item.mixinName}`);
|
||||
if (childId && parentId) {
|
||||
const relId = generateId('IMPLEMENTS', `${childId}->${parentId}`);
|
||||
graph.addRelationship({
|
||||
id: relId, sourceId: childId, targetId: parentId,
|
||||
type: 'IMPLEMENTS', confidence: 1.0, reason: 'trait-impl',
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
case 'properties':
|
||||
for (const item of routed.items) {
|
||||
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
||||
graph.addNode({
|
||||
id: nodeId,
|
||||
label: 'Property' as any,
|
||||
properties: {
|
||||
name: item.propName, filePath: file.path,
|
||||
startLine: item.startLine, endLine: item.endLine,
|
||||
language: SupportedLanguages.Ruby, isExported: true,
|
||||
description: item.accessorType,
|
||||
},
|
||||
});
|
||||
symbolTable.add(file.path, item.propName, nodeId, 'Property');
|
||||
const fileId = generateId('File', file.path);
|
||||
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
||||
graph.addRelationship({
|
||||
id: relId, sourceId: fileId, targetId: nodeId,
|
||||
type: 'DEFINES', confidence: 1.0, reason: '',
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
case 'call':
|
||||
break; // fall through to normal call processing below
|
||||
}
|
||||
}
|
||||
|
||||
// Skip common built-ins and noise
|
||||
if (isBuiltInOrNoise(calledName)) return;
|
||||
|
||||
@@ -272,10 +200,10 @@ export const processCalls = async (
|
||||
// 5. Find the enclosing function (caller)
|
||||
const callNode = captureMap['call'];
|
||||
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
|
||||
|
||||
|
||||
// Use enclosing function as source, fallback to file for top-level calls
|
||||
const sourceId = enclosingFuncId || generateId('File', file.path);
|
||||
|
||||
|
||||
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
||||
|
||||
graph.addRelationship({
|
||||
@@ -783,72 +711,37 @@ const resolveCallTarget = (
|
||||
* Filter out common built-in functions and noise
|
||||
* that shouldn't be tracked as calls
|
||||
*/
|
||||
/** Pre-built set (module-level singleton) to avoid re-creating per call */
|
||||
const BUILT_IN_NAMES = new Set([
|
||||
// JavaScript/TypeScript built-ins
|
||||
'console', 'log', 'warn', 'error', 'info', 'debug',
|
||||
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
||||
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
||||
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
|
||||
'JSON', 'parse', 'stringify',
|
||||
'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
|
||||
'Map', 'Set', 'WeakMap', 'WeakSet',
|
||||
'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
|
||||
'Math', 'Date', 'RegExp', 'Error',
|
||||
'require', 'import', 'export',
|
||||
'fetch', 'Response', 'Request',
|
||||
// React hooks and common functions
|
||||
'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext',
|
||||
'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue',
|
||||
'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy',
|
||||
// Common array/object methods
|
||||
'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every',
|
||||
'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split',
|
||||
'push', 'pop', 'shift', 'unshift', 'sort', 'reverse',
|
||||
'keys', 'values', 'entries', 'assign', 'freeze', 'seal',
|
||||
'hasOwnProperty', 'toString', 'valueOf',
|
||||
// Python built-ins
|
||||
'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
|
||||
'open', 'read', 'write', 'close', 'append', 'extend', 'update',
|
||||
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
|
||||
'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
|
||||
// C/C++ standard library and common kernel helpers
|
||||
'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
|
||||
'scanf', 'fscanf', 'sscanf',
|
||||
'malloc', 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp',
|
||||
'strlen', 'strcpy', 'strncpy', 'strcat', 'strncat', 'strcmp', 'strncmp', 'strstr', 'strchr', 'strrchr',
|
||||
'atoi', 'atol', 'atof', 'strtol', 'strtoul', 'strtoll', 'strtoull', 'strtod',
|
||||
'sizeof', 'offsetof', 'typeof',
|
||||
'assert', 'abort', 'exit', '_exit',
|
||||
'fopen', 'fclose', 'fread', 'fwrite', 'fseek', 'ftell', 'rewind', 'fflush', 'fgets', 'fputs',
|
||||
// Linux kernel common macros/helpers (not real call targets)
|
||||
'likely', 'unlikely', 'BUG', 'BUG_ON', 'WARN', 'WARN_ON', 'WARN_ONCE',
|
||||
'IS_ERR', 'PTR_ERR', 'ERR_PTR', 'IS_ERR_OR_NULL',
|
||||
'ARRAY_SIZE', 'container_of', 'list_for_each_entry', 'list_for_each_entry_safe',
|
||||
'min', 'max', 'clamp', 'abs', 'swap',
|
||||
'pr_info', 'pr_warn', 'pr_err', 'pr_debug', 'pr_notice', 'pr_crit', 'pr_emerg',
|
||||
'printk', 'dev_info', 'dev_warn', 'dev_err', 'dev_dbg',
|
||||
'GFP_KERNEL', 'GFP_ATOMIC',
|
||||
'spin_lock', 'spin_unlock', 'spin_lock_irqsave', 'spin_unlock_irqrestore',
|
||||
'mutex_lock', 'mutex_unlock', 'mutex_init',
|
||||
'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree',
|
||||
'get', 'put',
|
||||
// Ruby built-ins and Kernel methods
|
||||
'puts', 'print', 'p', 'pp', 'warn', 'raise', 'fail',
|
||||
'require', 'require_relative', 'load', 'autoload',
|
||||
'include', 'extend', 'prepend',
|
||||
'attr_accessor', 'attr_reader', 'attr_writer',
|
||||
'public', 'private', 'protected', 'module_function',
|
||||
'lambda', 'proc', 'block_given?',
|
||||
'nil?', 'is_a?', 'kind_of?', 'instance_of?', 'respond_to?',
|
||||
'freeze', 'frozen?', 'dup', 'clone', 'tap', 'then', 'yield_self',
|
||||
// Ruby enumerables
|
||||
'each', 'map', 'select', 'reject', 'find', 'detect', 'collect',
|
||||
'inject', 'reduce', 'flat_map', 'each_with_object', 'each_with_index',
|
||||
'any?', 'all?', 'none?', 'count', 'first', 'last',
|
||||
'sort', 'sort_by', 'min', 'max', 'min_by', 'max_by',
|
||||
'group_by', 'partition', 'zip', 'compact', 'flatten', 'uniq',
|
||||
]);
|
||||
const isBuiltInOrNoise = (name: string): boolean => {
|
||||
const builtIns = new Set([
|
||||
// JavaScript/TypeScript built-ins
|
||||
'console', 'log', 'warn', 'error', 'info', 'debug',
|
||||
'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
|
||||
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
||||
'encodeURI', 'decodeURI', 'encodeURIComponent', 'decodeURIComponent',
|
||||
'JSON', 'parse', 'stringify',
|
||||
'Object', 'Array', 'String', 'Number', 'Boolean', 'Symbol', 'BigInt',
|
||||
'Map', 'Set', 'WeakMap', 'WeakSet',
|
||||
'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
|
||||
'Math', 'Date', 'RegExp', 'Error',
|
||||
'require', 'import', 'export',
|
||||
'fetch', 'Response', 'Request',
|
||||
// React hooks and common functions
|
||||
'useState', 'useEffect', 'useCallback', 'useMemo', 'useRef', 'useContext',
|
||||
'useReducer', 'useLayoutEffect', 'useImperativeHandle', 'useDebugValue',
|
||||
'createElement', 'createContext', 'createRef', 'forwardRef', 'memo', 'lazy',
|
||||
// Common array/object methods
|
||||
'map', 'filter', 'reduce', 'forEach', 'find', 'findIndex', 'some', 'every',
|
||||
'includes', 'indexOf', 'slice', 'splice', 'concat', 'join', 'split',
|
||||
'push', 'pop', 'shift', 'unshift', 'sort', 'reverse',
|
||||
'keys', 'values', 'entries', 'assign', 'freeze', 'seal',
|
||||
'hasOwnProperty', 'toString', 'valueOf',
|
||||
// Python built-ins
|
||||
'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
|
||||
'open', 'read', 'write', 'close', 'append', 'extend', 'update',
|
||||
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
|
||||
'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
|
||||
]);
|
||||
|
||||
const isBuiltInOrNoise = (name: string): boolean => BUILT_IN_NAMES.has(name);
|
||||
return builtIns.has(name);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { detectFrameworkFromPath } from './framework-detection';
|
||||
|
||||
// ============================================================================
|
||||
// NAME PATTERNS - All 11 supported languages
|
||||
// NAME PATTERNS - All 9 supported languages
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
@@ -143,13 +143,6 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
|
||||
/^save$/, // Repository::save()
|
||||
/^delete$/, // Repository::delete()
|
||||
],
|
||||
|
||||
// Ruby
|
||||
'ruby': [
|
||||
/^call$/, // Service objects (MyService.call)
|
||||
/^perform$/, // Background jobs (Sidekiq, ActiveJob)
|
||||
/^execute$/, // Command pattern
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -309,12 +302,7 @@ export function isTestFile(filePath: string): boolean {
|
||||
p.endsWith('test.php') ||
|
||||
p.endsWith('spec.php') ||
|
||||
p.includes('/tests/feature/') ||
|
||||
p.includes('/tests/unit/') ||
|
||||
// Ruby test patterns
|
||||
p.endsWith('_spec.rb') ||
|
||||
p.endsWith('_test.rb') ||
|
||||
p.includes('/spec/') ||
|
||||
p.includes('/test/fixtures/')
|
||||
p.includes('/tests/unit/')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -257,17 +257,6 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
|
||||
return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' };
|
||||
}
|
||||
|
||||
// ========== RUBY ==========
|
||||
|
||||
// Ruby: bin/ or exe/ (CLI entry points)
|
||||
if ((p.includes('/bin/') || p.includes('/exe/')) && p.endsWith('.rb')) {
|
||||
return { framework: 'ruby', entryPointMultiplier: 2.5, reason: 'ruby-executable' };
|
||||
}
|
||||
|
||||
// Ruby: Rakefile or *.rake (task definitions)
|
||||
if (p.endsWith('/rakefile') || p.endsWith('.rake')) {
|
||||
return { framework: 'ruby', entryPointMultiplier: 1.5, reason: 'ruby-rake' };
|
||||
}
|
||||
// ========== SWIFT / iOS ==========
|
||||
|
||||
// iOS App entry points (highest priority)
|
||||
|
||||
@@ -53,9 +53,7 @@ const resolveImportPath = (
|
||||
// Go
|
||||
'.go',
|
||||
// Rust
|
||||
'.rs', '/mod.rs',
|
||||
// Ruby
|
||||
'.rb', '.rake',
|
||||
'.rs', '/mod.rs'
|
||||
];
|
||||
|
||||
if (importPath.startsWith('.')) {
|
||||
@@ -222,45 +220,6 @@ export const processImports = async (
|
||||
importMap.get(file.path)!.add(resolvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Ruby: require/require_relative come through @call, not @import ----
|
||||
if (language === 'ruby' && captureMap['call']) {
|
||||
const callNameNode = captureMap['call.name'];
|
||||
if (callNameNode) {
|
||||
const calledName = callNameNode.text;
|
||||
if (calledName === 'require' || calledName === 'require_relative') {
|
||||
const callNode = captureMap['call'];
|
||||
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');
|
||||
if (contentNode) {
|
||||
let importPath = contentNode.text;
|
||||
// require_relative always resolves relative to current file
|
||||
if (calledName === 'require_relative' && !importPath.startsWith('.')) {
|
||||
importPath = './' + importPath;
|
||||
}
|
||||
totalImportsFound++;
|
||||
const resolvedPath = resolveImportPath(
|
||||
file.path, importPath, allFilePaths, allFileList, resolveCache
|
||||
);
|
||||
if (resolvedPath) {
|
||||
const sourceId = generateId('File', file.path);
|
||||
const targetId = generateId('File', resolvedPath);
|
||||
const relId = generateId('IMPORTS', `${file.path}->${resolvedPath}`);
|
||||
totalImportsResolved++;
|
||||
graph.addRelationship({
|
||||
id: relId, sourceId, targetId,
|
||||
type: 'IMPORTS', confidence: 1.0, reason: '',
|
||||
});
|
||||
if (!importMap.has(file.path)) {
|
||||
importMap.set(file.path, new Set());
|
||||
}
|
||||
importMap.get(file.path)!.add(resolvedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If re-parsed just for this, delete the tree to save memory
|
||||
|
||||
@@ -14,7 +14,7 @@ export type FileProgressCallback = (current: number, total: number, filePath: st
|
||||
|
||||
/**
|
||||
* Check if a symbol (function, class, etc.) is exported/public
|
||||
* Handles all 11 supported languages with explicit logic
|
||||
* Handles all 9 supported languages with explicit logic
|
||||
*
|
||||
* @param node - The AST node for the symbol name
|
||||
* @param name - The symbol name
|
||||
@@ -104,11 +104,7 @@ const isNodeExported = (node: any, name: string, language: string): boolean => {
|
||||
case 'c':
|
||||
case 'cpp':
|
||||
return false;
|
||||
|
||||
// Ruby: All top-level definitions are public by default
|
||||
case 'ruby':
|
||||
return true;
|
||||
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Shared Ruby call routing logic.
|
||||
*
|
||||
* Ruby expresses imports, heritage (mixins), and property definitions as
|
||||
* method calls rather than syntax-level constructs. This module provides a
|
||||
* single routing function used by the CLI call-processor, CLI parse-worker,
|
||||
* and the web call-processor so that the classification logic lives in one
|
||||
* place.
|
||||
*/
|
||||
|
||||
// ── Result types ────────────────────────────────────────────────────────────
|
||||
|
||||
export type RubyCallRouting =
|
||||
| { kind: 'import'; importPath: string; isRelative: boolean }
|
||||
| { kind: 'heritage'; items: RubyHeritageItem[] }
|
||||
| { kind: 'properties'; items: RubyPropertyItem[] }
|
||||
| { kind: 'call' }
|
||||
| { kind: 'skip' };
|
||||
|
||||
export interface RubyHeritageItem {
|
||||
enclosingClass: string;
|
||||
mixinName: string;
|
||||
}
|
||||
|
||||
export interface RubyPropertyItem {
|
||||
propName: string;
|
||||
accessorType: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
}
|
||||
|
||||
// ── Routing function ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Classify a Ruby call node and extract its semantic payload.
|
||||
*
|
||||
* @param calledName - The method name (e.g. 'require', 'include', 'attr_accessor')
|
||||
* @param callNode - The tree-sitter `call` AST node
|
||||
* @returns A discriminated union describing the call's semantic role
|
||||
*/
|
||||
export function routeRubyCall(calledName: string, callNode: any): RubyCallRouting {
|
||||
// ── 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');
|
||||
if (!contentNode) return { kind: 'skip' };
|
||||
|
||||
let importPath: string = contentNode.text;
|
||||
const isRelative = calledName === 'require_relative';
|
||||
if (isRelative && !importPath.startsWith('.')) {
|
||||
importPath = './' + importPath;
|
||||
}
|
||||
return { kind: 'import', importPath, isRelative };
|
||||
}
|
||||
|
||||
// ── include / extend / prepend → heritage (mixin) ──────────────────────
|
||||
if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
|
||||
let enclosingClass: string | null = null;
|
||||
let current = callNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'class' || current.type === 'module') {
|
||||
const nameNode = current.childForFieldName?.('name');
|
||||
if (nameNode) { enclosingClass = nameNode.text; break; }
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
if (!enclosingClass) return { kind: 'skip' };
|
||||
|
||||
const items: RubyHeritageItem[] = [];
|
||||
const argList = callNode.childForFieldName?.('arguments');
|
||||
for (const arg of (argList?.children ?? [])) {
|
||||
if (arg.type === 'constant' || arg.type === 'scope_resolution') {
|
||||
items.push({ enclosingClass, mixinName: arg.text });
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? { kind: 'heritage', items } : { kind: 'skip' };
|
||||
}
|
||||
|
||||
// ── attr_accessor / attr_reader / attr_writer → property definitions ───
|
||||
if (calledName === 'attr_accessor' || calledName === 'attr_reader' || calledName === 'attr_writer') {
|
||||
const items: RubyPropertyItem[] = [];
|
||||
const argList = callNode.childForFieldName?.('arguments');
|
||||
for (const arg of (argList?.children ?? [])) {
|
||||
if (arg.type === 'simple_symbol') {
|
||||
items.push({
|
||||
propName: arg.text.replace(/^:/, ''),
|
||||
accessorType: calledName,
|
||||
startLine: arg.startPosition.row,
|
||||
endLine: arg.endPosition.row,
|
||||
});
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? { kind: 'properties', items } : { kind: 'skip' };
|
||||
}
|
||||
|
||||
// ── Everything else → regular call ─────────────────────────────────────
|
||||
return { kind: 'call' };
|
||||
}
|
||||
@@ -396,40 +396,6 @@ export const PHP_QUERIES = `
|
||||
[(name) (qualified_name)] @heritage.trait))) @heritage
|
||||
`;
|
||||
|
||||
// Ruby queries - works with tree-sitter-ruby
|
||||
// NOTE: Ruby uses `call` for require, include, extend, prepend, attr_* etc.
|
||||
// These are all captured as @call and routed in JS post-processing:
|
||||
// - require/require_relative → import extraction
|
||||
// - include/extend/prepend → heritage (mixin) extraction
|
||||
// - attr_accessor/attr_reader/attr_writer → property definition extraction
|
||||
// - everything else → regular call extraction
|
||||
export const RUBY_QUERIES = `
|
||||
; ── Modules ──────────────────────────────────────────────────────────────────
|
||||
(module
|
||||
name: (constant) @name) @definition.module
|
||||
|
||||
; ── Classes ──────────────────────────────────────────────────────────────────
|
||||
(class
|
||||
name: (constant) @name) @definition.class
|
||||
|
||||
; ── Instance methods ─────────────────────────────────────────────────────────
|
||||
(method
|
||||
name: (identifier) @name) @definition.method
|
||||
|
||||
; ── Singleton (class-level) methods ──────────────────────────────────────────
|
||||
(singleton_method
|
||||
name: (identifier) @name) @definition.function
|
||||
|
||||
; ── All calls (require, include, attr_*, and regular calls routed in JS) ─────
|
||||
(call
|
||||
method: (identifier) @call.name) @call
|
||||
|
||||
; ── Heritage: class < SuperClass ─────────────────────────────────────────────
|
||||
(class
|
||||
name: (constant) @heritage.class
|
||||
superclass: (superclass
|
||||
(constant) @heritage.extends)) @heritage`;
|
||||
|
||||
// Swift queries - works with tree-sitter-swift
|
||||
export const SWIFT_QUERIES = `
|
||||
; Classes
|
||||
@@ -494,7 +460,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
||||
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
|
||||
[SupportedLanguages.Rust]: RUST_QUERIES,
|
||||
[SupportedLanguages.PHP]: PHP_QUERIES,
|
||||
[SupportedLanguages.Ruby]: RUBY_QUERIES,
|
||||
[SupportedLanguages.Swift]: SWIFT_QUERIES,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { SupportedLanguages } from '../../config/supported-languages';
|
||||
|
||||
/** Ruby extensionless filenames recognised as Ruby source */
|
||||
const RUBY_EXTENSIONLESS_FILES = new Set(['Rakefile', 'Gemfile', 'Guardfile', 'Vagrantfile', 'Brewfile']);
|
||||
|
||||
/**
|
||||
* Map file extension to SupportedLanguage enum
|
||||
*/
|
||||
@@ -34,15 +31,6 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages |
|
||||
filename.endsWith('.php5') || filename.endsWith('.php8')) {
|
||||
return SupportedLanguages.PHP;
|
||||
}
|
||||
// Ruby (extensions)
|
||||
if (filename.endsWith('.rb') || filename.endsWith('.rake') || filename.endsWith('.gemspec')) {
|
||||
return SupportedLanguages.Ruby;
|
||||
}
|
||||
// Ruby (extensionless files)
|
||||
const basename = filename.split('/').pop() || filename;
|
||||
if (RUBY_EXTENSIONLESS_FILES.has(basename)) {
|
||||
return SupportedLanguages.Ruby;
|
||||
}
|
||||
// Swift
|
||||
if (filename.endsWith('.swift')) return SupportedLanguages.Swift;
|
||||
return null;
|
||||
|
||||
@@ -40,7 +40,6 @@ const getWasmPath = (language: SupportedLanguages, filePath?: string): string =>
|
||||
[SupportedLanguages.Go]: '/wasm/go/tree-sitter-go.wasm',
|
||||
[SupportedLanguages.Rust]: '/wasm/rust/tree-sitter-rust.wasm',
|
||||
[SupportedLanguages.PHP]: '/wasm/php/tree-sitter-php.wasm',
|
||||
[SupportedLanguages.Ruby]: '/wasm/ruby/tree-sitter-ruby.wasm',
|
||||
[SupportedLanguages.Swift]: '/wasm/swift/tree-sitter-swift.wasm',
|
||||
};
|
||||
|
||||
|
||||
+1
-2
@@ -157,7 +157,7 @@ GitNexus supports indexing multiple repositories. Each `gitnexus analyze` regist
|
||||
|
||||
## Supported Languages
|
||||
|
||||
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby
|
||||
TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift
|
||||
|
||||
### Language Feature Matrix
|
||||
|
||||
@@ -173,7 +173,6 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift,
|
||||
| Rust | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ |
|
||||
| PHP | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | — |
|
||||
| Swift | — | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ |
|
||||
| Ruby | ✓ | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
|
||||
| C | — | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
|
||||
| C++ | — | ✓ | ✓ | — | — | ✓ | ✓ | ✓ |
|
||||
|
||||
|
||||
Generated
+2
-33
@@ -31,9 +31,9 @@
|
||||
"tree-sitter-go": "^0.21.0",
|
||||
"tree-sitter-java": "^0.21.0",
|
||||
"tree-sitter-javascript": "^0.21.0",
|
||||
"tree-sitter-kotlin": "^0.3.8",
|
||||
"tree-sitter-php": "^0.23.12",
|
||||
"tree-sitter-python": "^0.21.0",
|
||||
"tree-sitter-ruby": "^0.23.1",
|
||||
"tree-sitter-rust": "^0.21.0",
|
||||
"tree-sitter-typescript": "^0.21.0",
|
||||
"uuid": "^13.0.0"
|
||||
@@ -56,7 +56,6 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"tree-sitter-kotlin": "^0.3.8",
|
||||
"tree-sitter-swift": "^0.6.0"
|
||||
}
|
||||
},
|
||||
@@ -5416,7 +5415,6 @@
|
||||
"integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.1.0",
|
||||
"node-gyp-build": "^4.8.0"
|
||||
@@ -5434,8 +5432,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tree-sitter-php": {
|
||||
"version": "0.23.12",
|
||||
@@ -5490,34 +5487,6 @@
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tree-sitter-ruby": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-ruby/-/tree-sitter-ruby-0.23.1.tgz",
|
||||
"integrity": "sha512-d9/RXgWjR6HanN7wTYhS5bpBQLz1VkH048Vm3CodPGyJVnamXMGb8oEhDypVCBq4QnHui9sTXuJBBP3WtCw5RA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.2.2",
|
||||
"node-gyp-build": "^4.8.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tree-sitter": "^0.21.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"tree-sitter": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-ruby/node_modules/node-addon-api": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz",
|
||||
"integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-sitter-rust": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.21.0.tgz",
|
||||
|
||||
@@ -72,13 +72,11 @@
|
||||
"tree-sitter-kotlin": "^0.3.8",
|
||||
"tree-sitter-php": "^0.23.12",
|
||||
"tree-sitter-python": "^0.21.0",
|
||||
"tree-sitter-ruby": "^0.23.1",
|
||||
"tree-sitter-rust": "^0.21.0",
|
||||
"tree-sitter-typescript": "^0.21.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"tree-sitter-kotlin": "^0.3.8",
|
||||
"tree-sitter-swift": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+27
-15
@@ -10,6 +10,7 @@ import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { glob } from 'glob';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -240,8 +241,6 @@ async function setupOpenCode(result: SetupResult): Promise<void> {
|
||||
|
||||
// ─── Skill Installation ───────────────────────────────────────────
|
||||
|
||||
const SKILL_NAMES = ['gitnexus-exploring', 'gitnexus-debugging', 'gitnexus-impact-analysis', 'gitnexus-refactoring', 'gitnexus-guide', 'gitnexus-cli'];
|
||||
|
||||
/**
|
||||
* Install GitNexus skills to a target directory.
|
||||
* Each skill is installed as {targetDir}/gitnexus-{skillName}/SKILL.md
|
||||
@@ -255,25 +254,38 @@ async function installSkillsTo(targetDir: string): Promise<string[]> {
|
||||
const installed: string[] = [];
|
||||
const skillsRoot = path.join(__dirname, '..', '..', 'skills');
|
||||
|
||||
for (const skillName of SKILL_NAMES) {
|
||||
let flatFiles: string[] = [];
|
||||
let dirSkillFiles: string[] = [];
|
||||
try {
|
||||
[flatFiles, dirSkillFiles] = await Promise.all([
|
||||
glob('*.md', { cwd: skillsRoot }),
|
||||
glob('*/SKILL.md', { cwd: skillsRoot }),
|
||||
]);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const skillSources = new Map<string, { isDirectory: boolean }>();
|
||||
|
||||
for (const relPath of dirSkillFiles) {
|
||||
skillSources.set(path.dirname(relPath), { isDirectory: true });
|
||||
}
|
||||
for (const relPath of flatFiles) {
|
||||
const skillName = path.basename(relPath, '.md');
|
||||
if (!skillSources.has(skillName)) {
|
||||
skillSources.set(skillName, { isDirectory: false });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [skillName, source] of skillSources) {
|
||||
const skillDir = path.join(targetDir, skillName);
|
||||
|
||||
try {
|
||||
// Try directory-based skill first (skills/{name}/SKILL.md)
|
||||
const dirSource = path.join(skillsRoot, skillName);
|
||||
const dirSkillFile = path.join(dirSource, 'SKILL.md');
|
||||
|
||||
let isDirectory = false;
|
||||
try {
|
||||
const stat = await fs.stat(dirSource);
|
||||
isDirectory = stat.isDirectory();
|
||||
} catch { /* not a directory */ }
|
||||
|
||||
if (isDirectory) {
|
||||
if (source.isDirectory) {
|
||||
const dirSource = path.join(skillsRoot, skillName);
|
||||
await copyDirRecursive(dirSource, skillDir);
|
||||
installed.push(skillName);
|
||||
} else {
|
||||
// Fall back to flat file (skills/{name}.md)
|
||||
const flatSource = path.join(skillsRoot, `${skillName}.md`);
|
||||
const content = await fs.readFile(flatSource, 'utf-8');
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
|
||||
@@ -7,9 +7,9 @@ export enum SupportedLanguages {
|
||||
CPlusPlus = 'cpp',
|
||||
CSharp = 'csharp',
|
||||
Go = 'go',
|
||||
Ruby = 'ruby',
|
||||
Rust = 'rust',
|
||||
PHP = 'php',
|
||||
Kotlin = 'kotlin',
|
||||
// Ruby = 'ruby',
|
||||
Swift = 'swift',
|
||||
}
|
||||
@@ -2,13 +2,12 @@ import { KnowledgeGraph } from '../graph/types.js';
|
||||
import { ASTCache } from './ast-cache.js';
|
||||
import type { SymbolDefinition, SymbolTable } from './symbol-table.js';
|
||||
import { ImportMap, PackageMap, NamedImportMap, isFileInPackageDir } from './import-processor.js';
|
||||
import { resolveSymbolInternal } from './symbol-resolver.js';
|
||||
import { resolveSymbol, resolveSymbolInternal } from './symbol-resolver.js';
|
||||
import { walkBindingChain } from './named-binding-extraction.js';
|
||||
import Parser from 'tree-sitter';
|
||||
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
||||
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
||||
import { generateId } from '../../lib/utils.js';
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
import {
|
||||
getLanguageFromFilename,
|
||||
isVerboseIngestionEnabled,
|
||||
@@ -22,8 +21,7 @@ import {
|
||||
} from './utils.js';
|
||||
import { buildTypeEnv, lookupTypeEnv } from './type-env.js';
|
||||
import { getTreeSitterBufferSize } from './constants.js';
|
||||
import type { ExtractedCall, ExtractedHeritage, ExtractedRoute } from './workers/parse-worker.js';
|
||||
import { routeRubyCall } from './ruby-call-routing.js';
|
||||
import type { ExtractedCall, ExtractedRoute } from './workers/parse-worker.js';
|
||||
|
||||
/**
|
||||
* Walk up the AST from a node to find the enclosing function/method.
|
||||
@@ -62,9 +60,8 @@ export const processCalls = async (
|
||||
packageMap?: PackageMap,
|
||||
onProgress?: (current: number, total: number) => void,
|
||||
namedImportMap?: NamedImportMap,
|
||||
): Promise<ExtractedHeritage[]> => {
|
||||
) => {
|
||||
const parser = await loadParser();
|
||||
const collectedHeritage: ExtractedHeritage[] = [];
|
||||
const logSkipped = isVerboseIngestionEnabled();
|
||||
const skippedByLang = logSkipped ? new Map<string, number>() : null;
|
||||
|
||||
@@ -135,55 +132,6 @@ export const processCalls = async (
|
||||
|
||||
const calledName = nameNode.text;
|
||||
|
||||
// Ruby: route special calls to heritage or properties (imports handled by import-processor)
|
||||
if (language === SupportedLanguages.Ruby) {
|
||||
const callNode = captureMap['call'];
|
||||
const routed = routeRubyCall(calledName, callNode);
|
||||
|
||||
switch (routed.kind) {
|
||||
case 'skip':
|
||||
case 'import': // handled by import-processor
|
||||
return;
|
||||
|
||||
case 'heritage':
|
||||
for (const item of routed.items) {
|
||||
collectedHeritage.push({
|
||||
filePath: file.path,
|
||||
className: item.enclosingClass,
|
||||
parentName: item.mixinName,
|
||||
kind: 'trait-impl',
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
case 'properties':
|
||||
for (const item of routed.items) {
|
||||
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
||||
graph.addNode({
|
||||
id: nodeId,
|
||||
label: 'Property' as any,
|
||||
properties: {
|
||||
name: item.propName, filePath: file.path,
|
||||
startLine: item.startLine, endLine: item.endLine,
|
||||
language: SupportedLanguages.Ruby, isExported: true,
|
||||
description: item.accessorType,
|
||||
},
|
||||
});
|
||||
symbolTable.add(file.path, item.propName, nodeId, 'Property');
|
||||
const fileId = generateId('File', file.path);
|
||||
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
||||
graph.addRelationship({
|
||||
id: relId, sourceId: fileId, targetId: nodeId,
|
||||
type: 'DEFINES', confidence: 1.0, reason: '',
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
||||
case 'call':
|
||||
break; // fall through to normal call processing below
|
||||
}
|
||||
}
|
||||
|
||||
// Skip common built-ins and noise
|
||||
if (isBuiltInOrNoise(calledName)) return;
|
||||
|
||||
@@ -204,10 +152,10 @@ export const processCalls = async (
|
||||
|
||||
// 5. Find the enclosing function (caller)
|
||||
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
|
||||
|
||||
|
||||
// Use enclosing function as source, fallback to file for top-level calls
|
||||
const sourceId = enclosingFuncId || generateId('File', file.path);
|
||||
|
||||
|
||||
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
||||
|
||||
graph.addRelationship({
|
||||
@@ -230,8 +178,6 @@ export const processCalls = async (
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return collectedHeritage;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@ import { detectFrameworkFromPath } from './framework-detection.js';
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
|
||||
// ============================================================================
|
||||
// NAME PATTERNS - All 11 supported languages
|
||||
// NAME PATTERNS - All 9 supported languages
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
@@ -191,13 +191,6 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
|
||||
/^save$/, // Repository::save()
|
||||
/^delete$/, // Repository::delete()
|
||||
],
|
||||
|
||||
// Ruby
|
||||
'ruby': [
|
||||
/^call$/, // Service objects (MyService.call)
|
||||
/^perform$/, // Background jobs (Sidekiq, ActiveJob)
|
||||
/^execute$/, // Command pattern
|
||||
],
|
||||
};
|
||||
|
||||
/** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
|
||||
@@ -368,12 +361,7 @@ export function isTestFile(filePath: string): boolean {
|
||||
p.endsWith('test.php') ||
|
||||
p.endsWith('spec.php') ||
|
||||
p.includes('/tests/feature/') ||
|
||||
p.includes('/tests/unit/') ||
|
||||
// Ruby test patterns
|
||||
p.endsWith('_spec.rb') ||
|
||||
p.endsWith('_test.rb') ||
|
||||
p.includes('/spec/') ||
|
||||
p.includes('/test/fixtures/')
|
||||
p.includes('/tests/unit/')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,6 @@ const exportCheckers = {
|
||||
[SupportedLanguages.CPlusPlus]: cCppExportChecker,
|
||||
[SupportedLanguages.PHP]: phpExportChecker,
|
||||
[SupportedLanguages.Swift]: swiftExportChecker,
|
||||
[SupportedLanguages.Ruby]: (_node, _name) => true,
|
||||
} satisfies Record<SupportedLanguages, ExportChecker>;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -330,18 +330,6 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null
|
||||
return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' };
|
||||
}
|
||||
|
||||
// ========== RUBY ==========
|
||||
|
||||
// Ruby: bin/ or exe/ (CLI entry points)
|
||||
if ((p.includes('/bin/') || p.includes('/exe/')) && p.endsWith('.rb')) {
|
||||
return { framework: 'ruby', entryPointMultiplier: 2.5, reason: 'ruby-executable' };
|
||||
}
|
||||
|
||||
// Ruby: Rakefile or *.rake (task definitions)
|
||||
if (p.endsWith('/rakefile') || p.endsWith('.rake')) {
|
||||
return { framework: 'ruby', entryPointMultiplier: 1.5, reason: 'ruby-rake' };
|
||||
}
|
||||
|
||||
// ========== SWIFT / iOS ==========
|
||||
|
||||
// iOS App entry points (highest priority)
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
resolveCSharpNamespaceDir,
|
||||
resolvePhpImport,
|
||||
resolveRustImport,
|
||||
extractRubyImportPath,
|
||||
} from './resolvers/index.js';
|
||||
import type {
|
||||
SuffixIndex,
|
||||
@@ -445,24 +444,6 @@ export const processImports = async (
|
||||
const bindings = namedImportMap ? extractNamedBindings(captureMap['import'], language) : undefined;
|
||||
applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
|
||||
}
|
||||
|
||||
// ---- Ruby: require/require_relative come through @call, not @import ----
|
||||
if (language === SupportedLanguages.Ruby && captureMap['call']) {
|
||||
const callNameNode = captureMap['call.name'];
|
||||
if (callNameNode) {
|
||||
const extracted = extractRubyImportPath(callNameNode.text, captureMap['call']);
|
||||
if (extracted) {
|
||||
totalImportsFound++;
|
||||
const resolvedPath = resolveImportPath(
|
||||
file.path, extracted.importPath, allFilePaths, allFileList,
|
||||
normalizedFileList, resolveCache, language, configs.tsconfigPaths, index,
|
||||
);
|
||||
if (resolvedPath) {
|
||||
addImportEdge(file.path, resolvedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tree is now owned by the LRU cache — no manual delete needed
|
||||
|
||||
@@ -273,11 +273,8 @@ export const runPipelineFromRepo = async (
|
||||
.filter(p => chunkContents.has(p))
|
||||
.map(p => ({ path: p, content: chunkContents.get(p)! }));
|
||||
astCache = createASTCache(chunkFiles.length);
|
||||
const rubyHeritage = await processCalls(graph, chunkFiles, astCache, symbolTable, importMap, packageMap, undefined, namedImportMap);
|
||||
await processCalls(graph, chunkFiles, astCache, symbolTable, importMap, packageMap, undefined, namedImportMap);
|
||||
await processHeritage(graph, chunkFiles, astCache, symbolTable, importMap, packageMap);
|
||||
if (rubyHeritage.length > 0) {
|
||||
await processHeritageFromExtracted(graph, rubyHeritage, symbolTable, importMap, packageMap);
|
||||
}
|
||||
astCache.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,5 @@ export type { ComposerConfig } from './php.js';
|
||||
|
||||
export { resolveRustImport, tryRustModulePath } from './rust.js';
|
||||
|
||||
export { resolveRubyImport, extractRubyImportPath } from './ruby.js';
|
||||
export type { GemfileConfig } from './ruby.js';
|
||||
|
||||
export { resolveImportPath, RESOLVE_CACHE_CAP } from './standard.js';
|
||||
export type { TsconfigPaths } from './standard.js';
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Ruby require/require_relative import resolution.
|
||||
* Handles path resolution for Ruby's require and require_relative calls.
|
||||
*/
|
||||
|
||||
import type { SuffixIndex } from './utils.js';
|
||||
import { suffixResolve } from './utils.js';
|
||||
|
||||
/** Bundler config parsed from Gemfile (future: gem path resolution) */
|
||||
export interface GemfileConfig {
|
||||
gemPaths: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Ruby require/require_relative path to a matching .rb file.
|
||||
*
|
||||
* require_relative paths are pre-normalized to './' prefix by the caller.
|
||||
* require paths use suffix matching (gem-style paths like 'json', 'net/http').
|
||||
*/
|
||||
export function resolveRubyImport(
|
||||
sourceFile: string,
|
||||
importPath: string,
|
||||
isRelative: boolean,
|
||||
normalizedFileList: string[],
|
||||
allFileList: string[],
|
||||
index?: SuffixIndex,
|
||||
): string | null {
|
||||
const pathParts = importPath.replace(/^\.\//, '').split('/').filter(Boolean);
|
||||
return suffixResolve(pathParts, normalizedFileList, allFileList, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the import path string from a Ruby call AST node.
|
||||
* Returns null if the call is not a require/require_relative.
|
||||
*/
|
||||
export function extractRubyImportPath(
|
||||
calledName: string,
|
||||
callNode: any,
|
||||
): { importPath: string; isRelative: boolean } | null {
|
||||
if (calledName !== 'require' && calledName !== 'require_relative') return null;
|
||||
|
||||
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');
|
||||
if (!contentNode) return null;
|
||||
|
||||
let importPath = contentNode.text;
|
||||
const isRelative = calledName === 'require_relative';
|
||||
if (isRelative && !importPath.startsWith('.')) {
|
||||
importPath = './' + importPath;
|
||||
}
|
||||
|
||||
return { importPath, isRelative };
|
||||
}
|
||||
@@ -26,8 +26,6 @@ export const EXTENSIONS = [
|
||||
'.php', '.phtml',
|
||||
// Swift
|
||||
'.swift',
|
||||
// Ruby
|
||||
'.rb',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Shared Ruby call routing logic.
|
||||
*
|
||||
* Ruby expresses imports, heritage (mixins), and property definitions as
|
||||
* method calls rather than syntax-level constructs. This module provides a
|
||||
* single routing function used by the CLI call-processor, CLI parse-worker,
|
||||
* and the web call-processor so that the classification logic lives in one
|
||||
* place.
|
||||
*/
|
||||
|
||||
// ── Result types ────────────────────────────────────────────────────────────
|
||||
|
||||
export type RubyCallRouting =
|
||||
| { kind: 'import'; importPath: string; isRelative: boolean }
|
||||
| { kind: 'heritage'; items: RubyHeritageItem[] }
|
||||
| { kind: 'properties'; items: RubyPropertyItem[] }
|
||||
| { kind: 'call' }
|
||||
| { kind: 'skip' };
|
||||
|
||||
export interface RubyHeritageItem {
|
||||
enclosingClass: string;
|
||||
mixinName: string;
|
||||
}
|
||||
|
||||
export interface RubyPropertyItem {
|
||||
propName: string;
|
||||
accessorType: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
}
|
||||
|
||||
// ── Routing function ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Classify a Ruby call node and extract its semantic payload.
|
||||
*
|
||||
* @param calledName - The method name (e.g. 'require', 'include', 'attr_accessor')
|
||||
* @param callNode - The tree-sitter `call` AST node
|
||||
* @returns A discriminated union describing the call's semantic role
|
||||
*/
|
||||
export function routeRubyCall(calledName: string, callNode: any): RubyCallRouting {
|
||||
// ── 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');
|
||||
if (!contentNode) return { kind: 'skip' };
|
||||
|
||||
let importPath: string = contentNode.text;
|
||||
const isRelative = calledName === 'require_relative';
|
||||
if (isRelative && !importPath.startsWith('.')) {
|
||||
importPath = './' + importPath;
|
||||
}
|
||||
return { kind: 'import', importPath, isRelative };
|
||||
}
|
||||
|
||||
// ── include / extend / prepend → heritage (mixin) ──────────────────────
|
||||
if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
|
||||
let enclosingClass: string | null = null;
|
||||
let current = callNode.parent;
|
||||
while (current) {
|
||||
if (current.type === 'class' || current.type === 'module') {
|
||||
const nameNode = current.childForFieldName?.('name');
|
||||
if (nameNode) { enclosingClass = nameNode.text; break; }
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
if (!enclosingClass) return { kind: 'skip' };
|
||||
|
||||
const items: RubyHeritageItem[] = [];
|
||||
const argList = callNode.childForFieldName?.('arguments');
|
||||
for (const arg of (argList?.children ?? [])) {
|
||||
if (arg.type === 'constant' || arg.type === 'scope_resolution') {
|
||||
items.push({ enclosingClass, mixinName: arg.text });
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? { kind: 'heritage', items } : { kind: 'skip' };
|
||||
}
|
||||
|
||||
// ── attr_accessor / attr_reader / attr_writer → property definitions ───
|
||||
if (calledName === 'attr_accessor' || calledName === 'attr_reader' || calledName === 'attr_writer') {
|
||||
const items: RubyPropertyItem[] = [];
|
||||
const argList = callNode.childForFieldName?.('arguments');
|
||||
for (const arg of (argList?.children ?? [])) {
|
||||
if (arg.type === 'simple_symbol') {
|
||||
items.push({
|
||||
propName: arg.text.replace(/^:/, ''),
|
||||
accessorType: calledName,
|
||||
startLine: arg.startPosition.row,
|
||||
endLine: arg.endPosition.row,
|
||||
});
|
||||
}
|
||||
}
|
||||
return items.length > 0 ? { kind: 'properties', items } : { kind: 'skip' };
|
||||
}
|
||||
|
||||
// ── Everything else → regular call ─────────────────────────────────────
|
||||
return { kind: 'call' };
|
||||
}
|
||||
@@ -496,41 +496,6 @@ export const PHP_QUERIES = `
|
||||
[(name) (qualified_name)] @heritage.trait))) @heritage
|
||||
`;
|
||||
|
||||
// Ruby queries - works with tree-sitter-ruby
|
||||
// NOTE: Ruby uses `call` for require, include, extend, prepend, attr_* etc.
|
||||
// These are all captured as @call and routed in JS post-processing:
|
||||
// - require/require_relative → import extraction
|
||||
// - include/extend/prepend → heritage (mixin) extraction
|
||||
// - attr_accessor/attr_reader/attr_writer → property definition extraction
|
||||
// - everything else → regular call extraction
|
||||
export const RUBY_QUERIES = `
|
||||
; ── Modules ──────────────────────────────────────────────────────────────────
|
||||
(module
|
||||
name: (constant) @name) @definition.module
|
||||
|
||||
; ── Classes ──────────────────────────────────────────────────────────────────
|
||||
(class
|
||||
name: (constant) @name) @definition.class
|
||||
|
||||
; ── Instance methods ─────────────────────────────────────────────────────────
|
||||
(method
|
||||
name: (identifier) @name) @definition.method
|
||||
|
||||
; ── Singleton (class-level) methods ──────────────────────────────────────────
|
||||
(singleton_method
|
||||
name: (identifier) @name) @definition.function
|
||||
|
||||
; ── All calls (require, include, attr_*, and regular calls routed in JS) ─────
|
||||
(call
|
||||
method: (identifier) @call.name) @call
|
||||
|
||||
; ── Heritage: class < SuperClass ─────────────────────────────────────────────
|
||||
(class
|
||||
name: (constant) @heritage.class
|
||||
superclass: (superclass
|
||||
(constant) @heritage.extends)) @heritage
|
||||
`;
|
||||
|
||||
// Kotlin queries - works with tree-sitter-kotlin (fwcd/tree-sitter-kotlin)
|
||||
// Based on official tags.scm; functions use simple_identifier, classes use type_identifier
|
||||
export const KOTLIN_QUERIES = `
|
||||
@@ -678,7 +643,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
|
||||
[SupportedLanguages.Go]: GO_QUERIES,
|
||||
[SupportedLanguages.CPlusPlus]: CPP_QUERIES,
|
||||
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
|
||||
[SupportedLanguages.Ruby]: RUBY_QUERIES,
|
||||
[SupportedLanguages.Rust]: RUST_QUERIES,
|
||||
[SupportedLanguages.PHP]: PHP_QUERIES,
|
||||
[SupportedLanguages.Kotlin]: KOTLIN_QUERIES,
|
||||
|
||||
@@ -29,11 +29,6 @@ export const typeConfigs = {
|
||||
[SupportedLanguages.C]: cCppConfig,
|
||||
[SupportedLanguages.CPlusPlus]: cCppConfig,
|
||||
[SupportedLanguages.PHP]: phpConfig,
|
||||
[SupportedLanguages.Ruby]: {
|
||||
declarationNodeTypes: new Set<string>(),
|
||||
extractDeclaration: () => {},
|
||||
extractParameter: () => {},
|
||||
},
|
||||
} satisfies Record<SupportedLanguages, LanguageTypeConfig>;
|
||||
|
||||
export type { LanguageTypeConfig, TypeBindingExtractor, ParameterExtractor } from './types.js';
|
||||
|
||||
@@ -417,9 +417,6 @@ export const extractFunctionName = (node: any): { funcName: string | null; label
|
||||
*/
|
||||
export const yieldToEventLoop = (): Promise<void> => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
/** Ruby extensionless filenames recognised as Ruby source */
|
||||
const RUBY_EXTENSIONLESS_FILES = new Set(['Rakefile', 'Gemfile', 'Guardfile', 'Vagrantfile', 'Brewfile']);
|
||||
|
||||
/**
|
||||
* Find a child of `childType` within a sibling node of `siblingType`.
|
||||
* Used for Kotlin AST traversal where visibility_modifier lives inside a modifiers sibling.
|
||||
@@ -472,16 +469,6 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages |
|
||||
filename.endsWith('.php5') || filename.endsWith('.php8')) {
|
||||
return SupportedLanguages.PHP;
|
||||
}
|
||||
// Ruby (extensions)
|
||||
if (filename.endsWith('.rb') || filename.endsWith('.rake') || filename.endsWith('.gemspec')) {
|
||||
return SupportedLanguages.Ruby;
|
||||
}
|
||||
// Ruby (extensionless files)
|
||||
const basename = filename.split('/').pop() || filename;
|
||||
if (RUBY_EXTENSIONLESS_FILES.has(basename)) {
|
||||
return SupportedLanguages.Ruby;
|
||||
}
|
||||
// Swift (extensions)
|
||||
if (filename.endsWith('.swift')) return SupportedLanguages.Swift;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ import Go from 'tree-sitter-go';
|
||||
import Rust from 'tree-sitter-rust';
|
||||
import Kotlin from 'tree-sitter-kotlin';
|
||||
import PHP from 'tree-sitter-php';
|
||||
import Ruby from 'tree-sitter-ruby';
|
||||
import { createRequire } from 'node:module';
|
||||
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
||||
import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
|
||||
@@ -39,7 +38,6 @@ import { detectFrameworkFromAST } from '../framework-detection.js';
|
||||
import { generateId } from '../../../lib/utils.js';
|
||||
import { extractNamedBindings } from '../named-binding-extraction.js';
|
||||
import { appendKotlinWildcard } from '../resolvers/index.js';
|
||||
import { routeRubyCall } from '../ruby-call-routing.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types for serializable results
|
||||
@@ -157,7 +155,6 @@ const languageMap: Record<string, any> = {
|
||||
[SupportedLanguages.Rust]: Rust,
|
||||
[SupportedLanguages.Kotlin]: Kotlin,
|
||||
[SupportedLanguages.PHP]: PHP.php_only,
|
||||
[SupportedLanguages.Ruby]: Ruby,
|
||||
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
||||
};
|
||||
|
||||
@@ -861,91 +858,6 @@ const processFileGroup = (
|
||||
const callNameNode = captureMap['call.name'];
|
||||
if (callNameNode) {
|
||||
const calledName = callNameNode.text;
|
||||
|
||||
// Ruby: route special calls to imports, heritage, or properties
|
||||
if (language === SupportedLanguages.Ruby) {
|
||||
const callNode = captureMap['call'];
|
||||
const routed = routeRubyCall(calledName, callNode);
|
||||
|
||||
switch (routed.kind) {
|
||||
case 'skip':
|
||||
continue;
|
||||
|
||||
case 'import':
|
||||
result.imports.push({
|
||||
filePath: file.path,
|
||||
rawImportPath: routed.importPath,
|
||||
language,
|
||||
});
|
||||
continue;
|
||||
|
||||
case 'heritage':
|
||||
for (const item of routed.items) {
|
||||
result.heritage.push({
|
||||
filePath: file.path,
|
||||
className: item.enclosingClass,
|
||||
parentName: item.mixinName,
|
||||
kind: 'trait-impl',
|
||||
});
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'properties':
|
||||
for (const item of routed.items) {
|
||||
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
||||
result.nodes.push({
|
||||
id: nodeId,
|
||||
label: 'Property',
|
||||
properties: {
|
||||
name: item.propName,
|
||||
filePath: file.path,
|
||||
startLine: item.startLine,
|
||||
endLine: item.endLine,
|
||||
language,
|
||||
isExported: true,
|
||||
description: item.accessorType,
|
||||
},
|
||||
});
|
||||
result.symbols.push({
|
||||
filePath: file.path,
|
||||
name: item.propName,
|
||||
nodeId,
|
||||
type: 'Property',
|
||||
});
|
||||
const fileId = generateId('File', file.path);
|
||||
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
||||
result.relationships.push({
|
||||
id: relId,
|
||||
sourceId: fileId,
|
||||
targetId: nodeId,
|
||||
type: 'DEFINES',
|
||||
confidence: 1.0,
|
||||
reason: '',
|
||||
});
|
||||
}
|
||||
continue;
|
||||
|
||||
case 'call':
|
||||
if (!isBuiltInOrNoise(calledName)) {
|
||||
const sourceId = findEnclosingFunctionId(callNode, file.path)
|
||||
|| generateId('File', file.path);
|
||||
const callForm = inferCallForm(callNode, callNameNode);
|
||||
const receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
|
||||
const receiverTypeName = receiverName ? lookupTypeEnv(typeEnv, receiverName, callNode) : undefined;
|
||||
result.calls.push({
|
||||
filePath: file.path,
|
||||
calledName,
|
||||
sourceId,
|
||||
argCount: countCallArguments(callNode),
|
||||
...(callForm !== undefined ? { callForm } : {}),
|
||||
...(receiverName !== undefined ? { receiverName } : {}),
|
||||
...(receiverTypeName !== undefined ? { receiverTypeName } : {}),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBuiltInOrNoise(calledName)) {
|
||||
const callNode = captureMap['call'];
|
||||
const sourceId = findEnclosingFunctionId(callNode, file.path)
|
||||
|
||||
@@ -10,7 +10,6 @@ import Go from 'tree-sitter-go';
|
||||
import Rust from 'tree-sitter-rust';
|
||||
import Kotlin from 'tree-sitter-kotlin';
|
||||
import PHP from 'tree-sitter-php';
|
||||
import Ruby from 'tree-sitter-ruby';
|
||||
import { createRequire } from 'node:module';
|
||||
import { SupportedLanguages } from '../../config/supported-languages.js';
|
||||
|
||||
@@ -34,7 +33,6 @@ const languageMap: Record<string, any> = {
|
||||
[SupportedLanguages.Rust]: Rust,
|
||||
[SupportedLanguages.Kotlin]: Kotlin,
|
||||
[SupportedLanguages.PHP]: PHP.php_only,
|
||||
[SupportedLanguages.Ruby]: Ruby,
|
||||
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ export function isTestFilePath(filePath: string): boolean {
|
||||
p.includes('/test/') || p.includes('/tests/') ||
|
||||
p.includes('/testing/') || p.includes('/fixtures/') ||
|
||||
p.endsWith('_test.go') || p.endsWith('_test.py') ||
|
||||
p.endsWith('_spec.rb') || p.endsWith('_test.rb') || p.includes('/spec/') ||
|
||||
p.includes('/test_') || p.includes('/conftest.')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
class Handler
|
||||
def handle
|
||||
# handle request
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
class Handler
|
||||
def process
|
||||
# process request
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require_relative '../handlers/a/handler'
|
||||
|
||||
class Processor < Handler
|
||||
def run
|
||||
handle
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
class Person
|
||||
attr_accessor :name, :age
|
||||
attr_reader :id
|
||||
attr_writer :email
|
||||
|
||||
def greet
|
||||
"Hello, #{@name}"
|
||||
end
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
def write_audit(message)
|
||||
message
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
require_relative 'one'
|
||||
require_relative 'zero'
|
||||
|
||||
def run
|
||||
write_audit('hello')
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
def write_audit
|
||||
'default'
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
require_relative 'utils'
|
||||
|
||||
def save(data)
|
||||
# local save shadows utils#save
|
||||
data.to_s
|
||||
end
|
||||
|
||||
def run
|
||||
save('test')
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
def save(data)
|
||||
# remote save
|
||||
data
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
require_relative 'user'
|
||||
|
||||
def process_user
|
||||
user = User.new
|
||||
user.save
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
class User
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
module Auditable
|
||||
def audit
|
||||
# log changes
|
||||
end
|
||||
end
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
module Cacheable
|
||||
def cached
|
||||
# cache result
|
||||
end
|
||||
end
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
module Hookable
|
||||
def hooks
|
||||
# lifecycle hooks
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
require_relative '../concerns/auditable'
|
||||
require_relative '../concerns/cacheable'
|
||||
require_relative '../concerns/hookable'
|
||||
|
||||
class Record
|
||||
include Auditable
|
||||
extend Cacheable
|
||||
prepend Hookable
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
class BaseModel
|
||||
def save
|
||||
# persist
|
||||
end
|
||||
|
||||
def validate
|
||||
# validate
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
module Serializable
|
||||
def serialize
|
||||
to_s
|
||||
end
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
require_relative 'base_model'
|
||||
require_relative 'serializable'
|
||||
|
||||
class User < BaseModel
|
||||
include Serializable
|
||||
|
||||
def get_name
|
||||
@name
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
require_relative '../lib/user'
|
||||
|
||||
class AuthService
|
||||
def authenticate(user)
|
||||
user.validate
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
require_relative '../lib/base_model'
|
||||
|
||||
def process_model(model)
|
||||
model.validate
|
||||
model.save
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
require_relative 'user'
|
||||
require_relative 'repo'
|
||||
|
||||
def process_entities
|
||||
user = User.new
|
||||
repo = Repo.new
|
||||
user.save
|
||||
repo.save
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
class Repo
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
class User
|
||||
def save
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -742,8 +742,8 @@ describe('parsing', () => {
|
||||
|
||||
it('falls back gracefully for unsupported language', async () => {
|
||||
// getLanguageFromFilename returns null for extensions with no grammar mapping.
|
||||
const scalaLang = getLanguageFromFilename('Main.scala');
|
||||
expect(scalaLang).toBeNull();
|
||||
const rubyLang = getLanguageFromFilename('script.rb');
|
||||
expect(rubyLang).toBeNull();
|
||||
|
||||
const luaLang = getLanguageFromFilename('module.lua');
|
||||
expect(luaLang).toBeNull();
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
/**
|
||||
* Ruby: require/require_relative imports + class inheritance + mixin heritage +
|
||||
* attr_* properties + arity-filtered call resolution + member-call resolution
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES, getRelationships, getNodesByLabel, edgeSet,
|
||||
runPipelineFromRepo, type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Package: require_relative imports + class inheritance + module include
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby require_relative imports and class hierarchy', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-pkg'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects exactly 3 classes and 1 module', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['AuthService', 'BaseModel', 'User']);
|
||||
expect(getNodesByLabel(result, 'Module')).toEqual(['Serializable']);
|
||||
});
|
||||
|
||||
it('detects all 6 methods', () => {
|
||||
const methods = getNodesByLabel(result, 'Method');
|
||||
expect(methods).toContain('save');
|
||||
expect(methods).toContain('validate');
|
||||
expect(methods).toContain('serialize');
|
||||
expect(methods).toContain('get_name');
|
||||
expect(methods).toContain('authenticate');
|
||||
expect(methods).toContain('process_model');
|
||||
});
|
||||
|
||||
it('emits exactly 1 EXTENDS edge: User → BaseModel', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(extends_.length).toBe(1);
|
||||
expect(extends_[0].source).toBe('User');
|
||||
expect(extends_[0].target).toBe('BaseModel');
|
||||
expect(extends_[0].targetFilePath).toBe('lib/base_model.rb');
|
||||
});
|
||||
|
||||
it('emits exactly 1 IMPLEMENTS edge: User → Serializable (include)', () => {
|
||||
const implements_ = getRelationships(result, 'IMPLEMENTS');
|
||||
expect(implements_.length).toBe(1);
|
||||
expect(implements_[0].source).toBe('User');
|
||||
expect(implements_[0].target).toBe('Serializable');
|
||||
});
|
||||
|
||||
it('resolves all 4 require_relative imports', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
expect(imports.length).toBe(4);
|
||||
expect(edgeSet(imports)).toEqual([
|
||||
'auth_service.rb → user.rb',
|
||||
'helpers.rb → base_model.rb',
|
||||
'user.rb → base_model.rb',
|
||||
'user.rb → serializable.rb',
|
||||
]);
|
||||
});
|
||||
|
||||
it('no OVERRIDES edges target Property nodes', () => {
|
||||
const overrides = getRelationships(result, 'OVERRIDES');
|
||||
for (const edge of overrides) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
expect(target!.label).not.toBe('Property');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ambiguous: two files with same class name, require_relative disambiguates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby ambiguous symbol resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-ambiguous'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects 2 Handler classes', () => {
|
||||
const classes = getNodesByLabel(result, 'Class');
|
||||
expect(classes.filter(n => n === 'Handler').length).toBe(2);
|
||||
expect(classes).toContain('Processor');
|
||||
});
|
||||
|
||||
it('resolves EXTENDS to handlers/a/handler.rb (not handlers/b/handler.rb)', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(extends_.length).toBe(1);
|
||||
expect(extends_[0].source).toBe('Processor');
|
||||
expect(extends_[0].target).toBe('Handler');
|
||||
expect(extends_[0].targetFilePath).toBe('handlers/a/handler.rb');
|
||||
});
|
||||
|
||||
it('require_relative resolves to handlers/a/handler.rb', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
expect(imports.length).toBe(1);
|
||||
expect(imports[0].targetFilePath).toBe('handlers/a/handler.rb');
|
||||
});
|
||||
|
||||
it('all heritage edges point to real graph nodes', () => {
|
||||
for (const edge of getRelationships(result, 'EXTENDS')) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
expect(target!.properties.name).toBe(edge.target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Call resolution with arity filtering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby call resolution with arity filtering', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-calls'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves run → write_audit to one.rb via arity narrowing', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
expect(calls.length).toBe(1);
|
||||
expect(calls[0].source).toBe('run');
|
||||
expect(calls[0].target).toBe('write_audit');
|
||||
expect(calls[0].targetFilePath).toBe('one.rb');
|
||||
expect(calls[0].rel.reason).toBe('import-resolved');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Member-call resolution: obj.method() resolves through pipeline
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby member-call resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-member-calls'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User class (Class node) and save method (Method node)', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
// Ruby instance methods are Method nodes
|
||||
expect(getNodesByLabel(result, 'Method')).toContain('save');
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edge from User to save', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const edge = hasMethod.find(e => e.source === 'User' && e.target === 'save');
|
||||
expect(edge).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves process_user → save as a member call on User', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c => c.target === 'save');
|
||||
expect(saveCall).toBeDefined();
|
||||
expect(saveCall!.source).toBe('process_user');
|
||||
expect(saveCall!.targetFilePath).toBe('user.rb');
|
||||
});
|
||||
|
||||
it('emits 1 require_relative IMPORTS edge', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
expect(imports.length).toBe(1);
|
||||
expect(imports[0].source).toBe('app.rb');
|
||||
expect(imports[0].targetFilePath).toBe('user.rb');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mixin heritage: include / extend / prepend all produce IMPLEMENTS edges
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby mixin heritage (include / extend / prepend)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-mixin-heritage'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects 1 class and 3 modules', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toEqual(['Record']);
|
||||
expect(getNodesByLabel(result, 'Module')).toEqual(['Auditable', 'Cacheable', 'Hookable']);
|
||||
});
|
||||
|
||||
it('emits exactly 3 IMPLEMENTS edges for all three mixin types', () => {
|
||||
const implements_ = getRelationships(result, 'IMPLEMENTS');
|
||||
expect(implements_.length).toBe(3);
|
||||
expect(edgeSet(implements_)).toEqual([
|
||||
'Record → Auditable',
|
||||
'Record → Cacheable',
|
||||
'Record → Hookable',
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits no EXTENDS edges (mixins are not inheritance)', () => {
|
||||
const extends_ = getRelationships(result, 'EXTENDS');
|
||||
expect(extends_.length).toBe(0);
|
||||
});
|
||||
|
||||
it('resolves 3 require_relative IMPORTS edges', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
expect(imports.length).toBe(3);
|
||||
expect(edgeSet(imports)).toEqual([
|
||||
'record.rb → auditable.rb',
|
||||
'record.rb → cacheable.rb',
|
||||
'record.rb → hookable.rb',
|
||||
]);
|
||||
});
|
||||
|
||||
it('all IMPLEMENTS targets are real graph nodes (Module label)', () => {
|
||||
for (const edge of getRelationships(result, 'IMPLEMENTS')) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
expect(target!.label).toBe('Module');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Property extraction: attr_accessor / attr_reader / attr_writer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby attr_* property extraction', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-attr-properties'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects Person class and greet method', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Person');
|
||||
expect(getNodesByLabel(result, 'Method')).toContain('greet');
|
||||
});
|
||||
|
||||
it('creates 4 Property nodes for attr_accessor, attr_reader, attr_writer', () => {
|
||||
const props = getNodesByLabel(result, 'Property');
|
||||
expect(props).toEqual(['age', 'email', 'id', 'name']);
|
||||
});
|
||||
|
||||
it('all Property nodes have filePath pointing to person.rb', () => {
|
||||
result.graph.forEachNode(n => {
|
||||
if (n.label === 'Property') {
|
||||
expect(n.properties.filePath).toBe('models/person.rb');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('Property nodes carry accessorType in description field', () => {
|
||||
const accessorTypes = new Map<string, string>();
|
||||
result.graph.forEachNode(n => {
|
||||
if (n.label === 'Property') {
|
||||
accessorTypes.set(n.properties.name as string, n.properties.description as string);
|
||||
}
|
||||
});
|
||||
expect(accessorTypes.get('name')).toBe('attr_accessor');
|
||||
expect(accessorTypes.get('age')).toBe('attr_accessor');
|
||||
expect(accessorTypes.get('id')).toBe('attr_reader');
|
||||
expect(accessorTypes.get('email')).toBe('attr_writer');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Receiver-constrained resolution: two classes with same method name
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby receiver-constrained resolution', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-receiver-resolution'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('detects User and Repo classes, both with save methods', () => {
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
||||
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
|
||||
const saveMethods = getNodesByLabel(result, 'Method').filter(m => m === 'save');
|
||||
expect(saveMethods.length).toBe(2);
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edges for both User.save and Repo.save', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const userSave = hasMethod.find(e => e.source === 'User' && e.target === 'save');
|
||||
const repoSave = hasMethod.find(e => e.source === 'Repo' && e.target === 'save');
|
||||
expect(userSave).toBeDefined();
|
||||
expect(repoSave).toBeDefined();
|
||||
});
|
||||
|
||||
it('resolves at least one save call from process_entities', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCalls = calls.filter(c => c.target === 'save' && c.source === 'process_entities');
|
||||
expect(saveCalls.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('all heritage edges point to real graph nodes', () => {
|
||||
for (const edge of getRelationships(result, 'EXTENDS')) {
|
||||
const target = result.graph.getNode(edge.rel.targetId);
|
||||
expect(target).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local shadow: same-file definition takes priority over require'd name
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Ruby local definition shadows require', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'ruby-local-shadow'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('resolves run → save to same-file definition in main.rb, not utils.rb', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveCall = calls.find(c => c.target === 'save' && c.source === 'run');
|
||||
expect(saveCall).toBeDefined();
|
||||
expect(saveCall!.targetFilePath).toBe('src/main.rb');
|
||||
});
|
||||
|
||||
it('does NOT resolve save to utils.rb', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const saveToUtils = calls.find(c => c.target === 'save' && c.targetFilePath === 'src/utils.rb');
|
||||
expect(saveToUtils).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -93,23 +93,6 @@ describe('getLanguageFromFilename', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ruby', () => {
|
||||
it.each(['.rb', '.rake', '.gemspec'])(
|
||||
'detects %s files',
|
||||
(ext) => {
|
||||
expect(getLanguageFromFilename(`file${ext}`)).toBe(SupportedLanguages.Ruby);
|
||||
}
|
||||
);
|
||||
|
||||
it('detects extensionless Rakefile', () => {
|
||||
expect(getLanguageFromFilename('Rakefile')).toBe(SupportedLanguages.Ruby);
|
||||
});
|
||||
|
||||
it('detects extensionless Gemfile', () => {
|
||||
expect(getLanguageFromFilename('Gemfile')).toBe(SupportedLanguages.Ruby);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kotlin', () => {
|
||||
it.each(['.kt', '.kts'])(
|
||||
'detects %s files',
|
||||
@@ -120,7 +103,7 @@ describe('getLanguageFromFilename', () => {
|
||||
});
|
||||
|
||||
describe('unsupported', () => {
|
||||
it.each(['.scala', '.r', '.lua', '.zig', '.txt', '.md', '.json', '.yaml'])(
|
||||
it.each(['.rb', '.scala', '.r', '.lua', '.zig', '.txt', '.md', '.json', '.yaml'])(
|
||||
'returns null for %s files',
|
||||
(ext) => {
|
||||
expect(getLanguageFromFilename(`file${ext}`)).toBeNull();
|
||||
|
||||
@@ -67,12 +67,8 @@ describe('parser-loader', () => {
|
||||
await expect(loadLanguage(SupportedLanguages.TypeScript, 'utils.ts')).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('loads Ruby language', async () => {
|
||||
await expect(loadLanguage(SupportedLanguages.Ruby)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('throws for unsupported language', async () => {
|
||||
await expect(loadLanguage('erlang' as SupportedLanguages)).rejects.toThrow('Unsupported language');
|
||||
await expect(loadLanguage('ruby' as SupportedLanguages)).rejects.toThrow('Unsupported language');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('sequential native parser availability', () => {
|
||||
createASTCache(),
|
||||
createSymbolTable(),
|
||||
createImportMap(),
|
||||
)).resolves.toEqual([]);
|
||||
)).resolves.toBeUndefined();
|
||||
|
||||
expect(parserLoader.loadLanguage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user