From 8cc9a3160e6162605742b4c369c81c86e0b84891 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sat, 15 Nov 2025 02:23:20 +0000 Subject: [PATCH] svelte --- mcp_codebase.py | 351 +++++++++++++++++++++++--------- tools/parse_ts.js | 508 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 676 insertions(+), 183 deletions(-) diff --git a/mcp_codebase.py b/mcp_codebase.py index a4b27a8..c808889 100644 --- a/mcp_codebase.py +++ b/mcp_codebase.py @@ -1786,153 +1786,295 @@ def parse_java_file(filepath: Path) -> List[Dict]: # ----------------------------- @lru_cache(maxsize=500) def parse_svelte_file_cached(filepath_str: str, file_hash: str) -> tuple: - """Cached Svelte parsing with enhanced multi-language graph relationships.""" + """Cached Svelte parsing with enhanced multi-language graph relationships. + ALWAYS returns a tuple of (text, metadata_tuple) entries. If parsing + fails or produces no chunks, return a single fallback chunk containing + the whole file text and minimal metadata. + """ filepath = Path(filepath_str) chunks = [] + + logger.info(f"🎨 Parsing Svelte file: {filepath}") + try: text = filepath.read_text(encoding="utf-8") lines = text.splitlines(keepends=True) - # Track component props and context for graph relationships - component_props = [] - component_context = [] - # Extract component name from filename for graph relationships component_name = filepath.stem - if component_name[0].islower(): + if component_name and component_name[0].islower(): component_name = component_name[0].upper() + component_name[1:] # PascalCase - script_matches = list(re.finditer(r"]*)?>(.*?)", text, flags=re.DOTALL)) - idx = 0 + # Track component props and context for graph relationships + component_props = [] + component_stores = [] + component_imports = [] + reactive_declarations = [] - for m in script_matches: - script_content = m.group(1) - script_start_line = text[:m.start(1)].count("\n") + 1 - script_attrs = m.group(0).split('>')[0] # Get ", text, flags=re.DOTALL)) + script_idx = 0 - # Extract script context (module, context="module") - is_module = 'context="module"' in script_attrs + for script_match in script_matches: + script_attrs = script_match.group(1) or "" + script_content = script_match.group(2) + script_start_pos = script_match.start(2) + script_start_line = text[:script_start_pos].count("\n") + 1 + + # Determine script context + is_module = 'context="module"' in script_attrs or "context='module'" in script_attrs + is_typescript = 'lang="ts"' in script_attrs or "lang='ts'" in script_attrs script_type = "script_module" if is_module else "script" + script_lang = "typescript" if is_typescript else "javascript" + logger.info(f" Processing {script_type} block (lang: {script_lang})") + + # Try TypeScript/JavaScript parser ts_helper = Path("./tools/parse_ts.js") - if ts_helper.exists(): - tmp = filepath.parent / f".tmp_{filepath.name}_{idx}.ts" + parsed_with_helper = False + + if ts_helper.is_file(): + tmp = None try: + # Create temporary file for parsing + tmp = filepath.parent / f".tmp_{filepath.name}_{script_idx}.ts" tmp.write_text(script_content, encoding="utf-8") + proc = subprocess.run( ["node", str(ts_helper), str(tmp)], - capture_output=True, text=True, check=True, timeout=15 + capture_output=True, + text=True, + check=True, + timeout=15 ) decls = json.loads(proc.stdout) script_lines = script_content.splitlines(keepends=True) - for d in decls: + logger.info(f" TS helper found {len(decls)} declarations") + + for i, d in enumerate(decls): start_rel = max(0, d.get("start_line", 1) - 1) end_rel = d.get("end_line", start_rel + 1) + + # Guard the end_line + if end_rel <= start_rel: + end_rel = start_rel + 1 + chunk_code = "".join(script_lines[start_rel:end_rel]) doc = d.get("doc_comment", "").strip() abs_start = script_start_line + start_rel - # Extract component relationships - decl_name = d.get("name") - decl_type = d.get("type") + decl_name = d.get("name", "") + decl_type = d.get("type", "unknown") - # Track props for component interface - if decl_type == "variable" and decl_name and not is_module: - # Look for prop patterns: export let prop = ... - if re.search(r'export\s+let\s+' + re.escape(decl_name), chunk_code): + # Skip invalid declarations + if not decl_name or decl_type == "unknown": + continue + + # Analyze declaration for Svelte-specific patterns + is_exported = "export" in chunk_code + is_prop = False + is_reactive = False + is_store = False + + if decl_type == "variable" and not is_module: + # Check for prop: export let prop = ... + if re.search(r'\bexport\s+let\s+' + re.escape(decl_name), chunk_code): component_props.append(decl_name) + is_prop = True decl_type = "prop" - # Track reactive declarations: $: reactiveVar = ... - elif chunk_code.strip().startswith('$:'): - component_context.append(decl_name) + + # Check for reactive declaration: $: reactiveVar = ... + if chunk_code.strip().startswith('$:'): + reactive_declarations.append(decl_name) + is_reactive = True decl_type = "reactive" - chunk_text = f"File: {filepath}\nType: {decl_type}\nName: {decl_name}\nDoc: {doc}\nCode:\n{chunk_code}" + # Check for store: $storeName + if decl_name.startswith('$'): + component_stores.append(decl_name) + is_store = True + + # Track imports + if "import" in chunk_code and "from" in chunk_code: + import_match = re.search(r'from\s+["\']([^"\']+)["\']', chunk_code) + if import_match: + component_imports.append(import_match.group(1)) + + # Build chunk text + chunk_text = f"File: {filepath}\nType: {decl_type}\nName: {decl_name}\n" + chunk_text += f"Component: {component_name}\nScript Type: {script_type}\n" + if doc: + chunk_text += f"Doc:\n{doc}\n\n" + chunk_text += f"Code:\n{chunk_code}" metadata = { "file": str(filepath), "name": decl_name, "type": decl_type, "line": abs_start, - "language": "typescript", + "language": script_lang, "block_type": script_type, - "component_name": component_name + "component_name": component_name, } - # Add specific metadata for different declaration types - if decl_type == "prop": + # Add Svelte-specific metadata + if is_prop: metadata["is_exported"] = True metadata["component_prop"] = True - elif decl_type == "reactive": + if is_reactive: metadata["is_reactive"] = True - elif decl_type == "function": - metadata["is_function"] = True - # Try to detect event dispatchers + if is_store: + metadata["is_store"] = True + if is_exported and not is_prop: + metadata["is_exported"] = True + + # Function-specific metadata + if decl_type == "function": if "createEventDispatcher" in chunk_code: metadata["is_event_dispatcher"] = True + if "dispatch(" in chunk_code: + metadata["dispatches_events"] = True + + if doc: + metadata["docstring"] = doc chunks.append({ "text": chunk_text, "metadata": metadata }) + logger.info(f" Declaration {i+1}: {decl_type} {decl_name}") + + parsed_with_helper = True + + except subprocess.CalledProcessError as e: + logger.warning(f" TS parser subprocess failed for {filepath}: {e.stderr}") + except json.JSONDecodeError as e: + logger.warning(f" TS parser output invalid JSON for {filepath}: {e}") except Exception as e: - logger.warning(f"TS parser failed for {filepath}: {e}; falling back to heuristic") - chunks.append(_make_script_chunk(filepath, script_content, script_start_line, script_type, component_name)) + logger.warning(f" TS parser failed for {filepath}: {e}") finally: - tmp.unlink(missing_ok=True) - else: - chunks.append(_make_script_chunk(filepath, script_content, script_start_line, script_type, component_name)) - idx += 1 + if tmp and tmp.exists(): + tmp.unlink(missing_ok=True) - # Style blocks with CSS relationship extraction - for m in re.finditer(r"]*)?>(.*?)", text, flags=re.DOTALL): - style_content = m.group(1) - style_start_line = text[:m.start(1)].count("\n") + 1 + # Fallback: Create basic script chunk if parser failed + if not parsed_with_helper: + logger.info(f" Using fallback script chunk") + chunks.append(_make_script_chunk( + filepath, script_content, script_start_line, + script_type, component_name, script_lang + )) - # Extract CSS classes and relationships - css_classes = re.findall(r'\.([a-zA-Z][\w-]*)\s*{', style_content) - css_selectors = re.findall(r'([a-zA-Z][\w-]*)\s*{', style_content) + script_idx += 1 + + # Parse style blocks + style_matches = list(re.finditer(r"]*?))?>(.*?)", text, flags=re.DOTALL)) + + for style_idx, style_match in enumerate(style_matches): + style_attrs = style_match.group(1) or "" + style_content = style_match.group(2) + style_start_pos = style_match.start(2) + style_start_line = text[:style_start_pos].count("\n") + 1 + + # Determine style language + is_scss = 'lang="scss"' in style_attrs or "lang='scss'" in style_attrs + is_less = 'lang="less"' in style_attrs or "lang='less'" in style_attrs + is_scoped = "scoped" not in style_attrs # Svelte styles are scoped by default + + style_lang = "scss" if is_scss else ("less" if is_less else "css") + + # Extract CSS classes and selectors + css_classes = list(set(re.findall(r'\.([a-zA-Z][\w-]*)\s*[{,:]', style_content))) + css_ids = list(set(re.findall(r'#([a-zA-Z][\w-]*)\s*[{,:]', style_content))) + css_selectors = list(set(re.findall(r'^([a-zA-Z][\w-]*)\s*{', style_content, re.MULTILINE))) + + # Extract CSS custom properties (variables) + css_vars = list(set(re.findall(r'--([a-zA-Z][\w-]*)', style_content))) chunk_text = f"File: {filepath}\nType: style\nComponent: {component_name}\n" + chunk_text += f"Language: {style_lang}\nScoped: {is_scoped}\n" if css_classes: chunk_text += f"CSS Classes: {', '.join(css_classes)}\n" + if css_ids: + chunk_text += f"CSS IDs: {', '.join(css_ids)}\n" + if css_vars: + chunk_text += f"CSS Variables: {', '.join(css_vars)}\n" chunk_text += f"Style Content:\n{style_content}" chunks.append({ "text": chunk_text, "metadata": { "file": str(filepath), - "name": f"style_block_{idx}", + "name": f"style_block_{style_idx}", "type": "style", "line": style_start_line, - "language": "css", + "language": style_lang, "block_type": "style", "component_name": component_name, "css_classes": css_classes, - "css_selectors": css_selectors + "css_ids": css_ids, + "css_selectors": css_selectors, + "css_variables": css_vars, + "is_scoped": is_scoped, } }) - idx += 1 - # Markup with Svelte-specific element analysis - markup = re.sub(r"]*>.*?", "", text, flags=re.DOTALL) - markup = re.sub(r"]*>.*?", "", markup, flags=re.DOTALL).strip() + # Parse markup (template) + markup = text + # Remove script blocks + markup = re.sub(r"]*>.*?", "", markup, flags=re.DOTALL) + # Remove style blocks + markup = re.sub(r"]*>.*?", "", markup, flags=re.DOTALL) + markup = markup.strip() if markup: - # Extract component usage and bindings from markup - used_components = re.findall(r'<([A-Z][a-zA-Z]*)', markup) - prop_bindings = re.findall(r'(\w+)={([^}]+)}', markup) - event_handlers = re.findall(r'on:(\w+)=', markup) + # Extract Svelte-specific markup features + used_components = list(set(re.findall(r'<([A-Z][a-zA-Z0-9]*)', markup))) + prop_bindings = re.findall(r'\b(\w+)={([^}]+)}', markup) + event_handlers = list(set(re.findall(r'on:(\w+)=', markup))) + + # Extract slots + slots = list(set(re.findall(r' tuple: "language": "svelte", "block_type": "markup", "component_name": component_name, - "used_components": list(set(used_components)), + "used_components": used_components, "prop_bindings": [prop for prop, _ in prop_bindings], - "event_handlers": list(set(event_handlers)), - "component_props": component_props, # Props discovered in scripts - "reactive_vars": component_context # Reactive context discovered + "event_handlers": event_handlers, + "component_props": component_props, + "reactive_vars": reactive_declarations, + "stores": component_stores, + "template_stores": template_stores, + "imports": component_imports, + "slots": slots, + "has_default_slot": has_default_slot, + "use_directives": use_directives, + "transitions": transition_directives, + "bind_directives": bind_directives, } }) except Exception as e: - logger.warning(f"parse_svelte_file failed {filepath}: {e}") - return tuple() - return tuple((c["text"], tuple(c["metadata"].items())) for c in chunks) + logger.warning(f"Svelte parsing failed for {filepath}: {e}") -def _make_script_chunk(filepath, script_content, start_line, block_type, component_name=None): - """Enhanced script chunk creation with basic analysis.""" - # Basic heuristic analysis for when TypeScript parser fails + # Return parsed chunks or fallback + if chunks: + logger.info(f"✅ Successfully parsed {len(chunks)} chunks from {filepath}") + return tuple((c["text"], tuple(c["metadata"].items())) for c in chunks) + + # Fallback: emit a whole-file chunk + logger.warning(f"❌ No chunks created from Svelte parsing for {filepath}. Emitting fallback whole-file chunk.") + try: + full_code = filepath.read_text(encoding="utf-8") + except Exception: + full_code = "" + + fallback_meta = { + "file": str(filepath), + "name": filepath.stem, + "type": "component", + "line": 1, + "language": "svelte", + "component_name": filepath.stem, + } + return ((full_code, tuple(fallback_meta.items())),) + + +def _make_script_chunk(filepath: Path, script_content: str, start_line: int, + script_type: str, component_name: str, language: str = "javascript") -> dict: + """Create a fallback script chunk when TS parser fails.""" + chunk_text = f"File: {filepath}\nType: {script_type}\nComponent: {component_name}\n" + chunk_text += f"Language: {language}\nCode:\n{script_content}" + + # Extract basic info with regex exports = re.findall(r'export\s+(?:let|const|function|class)\s+(\w+)', script_content) - functions = re.findall(r'(?:export\s+)?function\s+(\w+)', script_content) - imports = re.findall(r'import.*from\s+[\'"]([^\'"]+)[\'"]', script_content) - - chunk_text = f"File: {filepath}\nType: {block_type}\n" - if component_name: - chunk_text += f"Component: {component_name}\n" - if exports: - chunk_text += f"Exports: {', '.join(exports)}\n" - if functions: - chunk_text += f"Functions: {', '.join(functions)}\n" - chunk_text += f"Code:\n{script_content}" + imports = re.findall(r'from\s+["\']([^"\']+)["\']', script_content) metadata = { "file": str(filepath), - "name": f"{block_type}_chunk", - "type": block_type, + "name": script_type, + "type": script_type, "line": start_line, - "language": "typescript" if block_type == "script" else "css", - "block_type": block_type + "language": language, + "block_type": script_type, + "component_name": component_name, } - if component_name: - metadata["component_name"] = component_name if exports: metadata["exports"] = exports if imports: metadata["imports"] = imports - return {"text": chunk_text, "metadata": metadata} + return { + "text": chunk_text, + "metadata": metadata + } def parse_svelte_file(filepath: Path) -> List[Dict]: """Parse Svelte file with caching.""" diff --git a/tools/parse_ts.js b/tools/parse_ts.js index 0c95667..2c617b9 100755 --- a/tools/parse_ts.js +++ b/tools/parse_ts.js @@ -1,9 +1,10 @@ #!/usr/bin/env node +// Enhanced TypeScript/JavaScript AST parser // Usage: node parse_ts.js file.ts -const fs = require('fs'); -const path = require('path'); -const { parse } = require('@typescript-eslint/typescript-estree'); +const fs = require("fs"); +const path = require("path"); +const { parse } = require("@typescript-eslint/typescript-estree"); function getLoc(node) { if (node.loc) { @@ -14,139 +15,459 @@ function getLoc(node) { function extractLeadingComment(node, sourceCode) { if (!node.range || !sourceCode) return ""; + const startIdx = node.range[0]; - let commentEnd = startIdx; - // Look backward for comments - let i = startIdx - 1; - let commentLines = []; - let inBlock = false; - while (i >= 0) { - const char = sourceCode[i]; - if (char === '\n') break; - i--; - } - const lineStart = i + 1; - const lineAbove = sourceCode.slice(lineStart, startIdx).trim(); - - // Check for // comment on same line before node - if (lineAbove.startsWith('//')) { - return lineAbove.substring(2).trim(); + // Find the start of the line containing the node + let lineStart = startIdx; + while (lineStart > 0 && sourceCode[lineStart - 1] !== "\n") { + lineStart--; } - // Look further up for multi-line or JSDoc - const lines = sourceCode.substring(0, lineStart).split('\n'); - for (let j = lines.length - 1; j >= Math.max(0, lines.length - 5); j--) { - const line = lines[j].trim(); - if (line.startsWith('//')) { - commentLines.unshift(line.substring(2).trim()); - } else if (line.endsWith('*/')) { - inBlock = true; - commentLines.unshift(line.slice(0, -2).trim()); - } else if (inBlock) { - if (line.startsWith('/*') || line.startsWith('/**')) { - commentLines.unshift(line.slice(2).trim()); + // Check for inline comment on the same line + const lineContent = sourceCode.slice(lineStart, startIdx).trim(); + if (lineContent.startsWith("//")) { + return lineContent.substring(2).trim(); + } + + // Look backward through previous lines for comments + const lines = sourceCode.substring(0, lineStart).split("\n"); + const commentLines = []; + let inBlockComment = false; + + for (let i = lines.length - 1; i >= Math.max(0, lines.length - 10); i--) { + const line = lines[i].trim(); + + if (line.endsWith("*/")) { + // End of block comment found + inBlockComment = true; + const blockContent = line + .slice(0, -2) + .replace(/^\*+\s*/, "") + .trim(); + if (blockContent) { + commentLines.unshift(blockContent); + } + } else if (inBlockComment) { + if (line.startsWith("/*") || line.startsWith("/**")) { + // Start of block comment found + const blockContent = line + .substring(line.indexOf("/*") + 2) + .replace(/^\*+\s*/, "") + .trim(); + if (blockContent) { + commentLines.unshift(blockContent); + } break; } else { - commentLines.unshift(line); + // Inside block comment + const blockContent = line.replace(/^\*+\s*/, "").trim(); + if (blockContent) { + commentLines.unshift(blockContent); + } + } + } else if (line.startsWith("//")) { + // Single-line comment + commentLines.unshift(line.substring(2).trim()); + } else if (line === "") { + // Empty line - continue if we already have comments + if (commentLines.length === 0) { + break; } - } else if (line === '') { - if (commentLines.length > 0) continue; - else break; } else { + // Non-comment, non-empty line - stop looking break; } } - return commentLines.join('\n').trim(); + + return commentLines.join("\n").trim(); +} + +function extractTypeInfo(node) { + const typeInfo = {}; + + // Extract type annotation + if (node.typeAnnotation) { + typeInfo.returnType = extractTypeString(node.typeAnnotation.typeAnnotation); + } + + // Extract parameters for functions + if (node.params) { + typeInfo.parameters = node.params.map((param) => { + const paramInfo = { + name: extractParamName(param), + type: param.typeAnnotation + ? extractTypeString(param.typeAnnotation.typeAnnotation) + : "any", + }; + if (param.optional) { + paramInfo.optional = true; + } + return paramInfo; + }); + } + + // Extract generics + if (node.typeParameters) { + typeInfo.generics = node.typeParameters.params.map((tp) => tp.name.name); + } + + return typeInfo; +} + +function extractParamName(param) { + if (param.type === "Identifier") { + return param.name; + } + if (param.type === "RestElement" && param.argument.type === "Identifier") { + return "..." + param.argument.name; + } + if (param.type === "AssignmentPattern" && param.left.type === "Identifier") { + return param.left.name; + } + return "unknown"; +} + +function extractTypeString(typeNode) { + if (!typeNode) return "any"; + + switch (typeNode.type) { + case "TSStringKeyword": + return "string"; + case "TSNumberKeyword": + return "number"; + case "TSBooleanKeyword": + return "boolean"; + case "TSAnyKeyword": + return "any"; + case "TSVoidKeyword": + return "void"; + case "TSUndefinedKeyword": + return "undefined"; + case "TSNullKeyword": + return "null"; + case "TSTypeReference": + if (typeNode.typeName.type === "Identifier") { + let typeStr = typeNode.typeName.name; + if (typeNode.typeParameters) { + const params = typeNode.typeParameters.params + .map(extractTypeString) + .join(", "); + typeStr += `<${params}>`; + } + return typeStr; + } + return "unknown"; + case "TSArrayType": + return extractTypeString(typeNode.elementType) + "[]"; + case "TSUnionType": + return typeNode.types.map(extractTypeString).join(" | "); + case "TSIntersectionType": + return typeNode.types.map(extractTypeString).join(" & "); + case "TSFunctionType": + return "Function"; + case "TSTypeLiteral": + return "object"; + default: + return "unknown"; + } } function extractDeclarations(ast, sourceCode) { const decls = []; + const parentMap = new WeakMap(); - function visit(node) { - if (!node || typeof node !== 'object') return; + function setParents(node, parent = null) { + if (!node || typeof node !== "object") return; if (Array.isArray(node)) { - node.forEach(n => visit(n)); + node.forEach((n) => setParents(n, parent)); return; } - let docComment = extractLeadingComment(node, sourceCode); + parentMap.set(node, parent); + Object.values(node).forEach((child) => setParents(child, node)); + } + + setParents(ast); + + function visit(node) { + if (!node || typeof node !== "object") return; + + if (Array.isArray(node)) { + node.forEach((n) => visit(n)); + return; + } + + const docComment = extractLeadingComment(node, sourceCode); + const loc = getLoc(node); + + // Function Declarations + if (node.type === "FunctionDeclaration" && node.id?.name) { + const typeInfo = extractTypeInfo(node); - if (node.type === 'FunctionDeclaration' && node.id?.name) { - const loc = getLoc(node); decls.push({ name: node.id.name, - type: 'function', + type: "function", doc_comment: docComment, start_line: loc.start, - end_line: loc.end + end_line: loc.end, + is_async: node.async || false, + is_generator: node.generator || false, + ...typeInfo, }); } + // Arrow Functions assigned to variables else if ( - node.type === 'VariableDeclarator' && - node.id?.type === 'Identifier' && - node.init?.type === 'ArrowFunctionExpression' + node.type === "VariableDeclarator" && + node.id?.type === "Identifier" && + node.init?.type === "ArrowFunctionExpression" ) { - const loc = getLoc(node); + const typeInfo = extractTypeInfo(node.init); + decls.push({ name: node.id.name, - type: 'function', + type: "function", doc_comment: docComment, start_line: loc.start, - end_line: loc.end + end_line: loc.end, + is_async: node.init.async || false, + is_arrow: true, + ...typeInfo, }); } - else if (node.type === 'ClassDeclaration' && node.id?.name) { - const loc = getLoc(node); + // Class Declarations + else if (node.type === "ClassDeclaration" && node.id?.name) { + const classInfo = { + name: node.id.name, + type: "class", + doc_comment: docComment, + start_line: loc.start, + end_line: loc.end, + is_abstract: node.abstract || false, + }; + + // Extract extends + if (node.superClass) { + if (node.superClass.type === "Identifier") { + classInfo.extends = node.superClass.name; + } + } + + // Extract implements + if (node.implements && node.implements.length > 0) { + classInfo.implements = node.implements.map( + (impl) => impl.expression.name || "unknown", + ); + } + + // Extract generics + if (node.typeParameters) { + classInfo.generics = node.typeParameters.params.map( + (tp) => tp.name.name, + ); + } + + // Extract methods and properties + const methods = []; + const properties = []; + + if (node.body && node.body.body) { + for (const member of node.body.body) { + if (member.type === "MethodDefinition" && member.key?.name) { + methods.push(member.key.name); + } else if (member.type === "PropertyDefinition" && member.key?.name) { + properties.push(member.key.name); + } + } + } + + if (methods.length > 0) { + classInfo.methods = methods; + } + if (properties.length > 0) { + classInfo.properties = properties; + } + + decls.push(classInfo); + } + // Interface Declarations + else if (node.type === "TSInterfaceDeclaration" && node.id?.name) { + const interfaceInfo = { + name: node.id.name, + type: "interface", + doc_comment: docComment, + start_line: loc.start, + end_line: loc.end, + }; + + // Extract extends + if (node.extends && node.extends.length > 0) { + interfaceInfo.extends = node.extends.map( + (ext) => ext.expression.name || "unknown", + ); + } + + // Extract generics + if (node.typeParameters) { + interfaceInfo.generics = node.typeParameters.params.map( + (tp) => tp.name.name, + ); + } + + // Extract properties/methods + const properties = []; + const methods = []; + + if (node.body && node.body.body) { + for (const member of node.body.body) { + if (member.type === "TSPropertySignature" && member.key?.name) { + properties.push(member.key.name); + } else if (member.type === "TSMethodSignature" && member.key?.name) { + methods.push(member.key.name); + } + } + } + + if (properties.length > 0) { + interfaceInfo.properties = properties; + } + if (methods.length > 0) { + interfaceInfo.methods = methods; + } + + decls.push(interfaceInfo); + } + // Type Alias Declarations + else if (node.type === "TSTypeAliasDeclaration" && node.id?.name) { + const typeInfo = { + name: node.id.name, + type: "type", + doc_comment: docComment, + start_line: loc.start, + end_line: loc.end, + }; + + // Extract generics + if (node.typeParameters) { + typeInfo.generics = node.typeParameters.params.map( + (tp) => tp.name.name, + ); + } + + // Extract type definition + if (node.typeAnnotation) { + typeInfo.type_definition = extractTypeString(node.typeAnnotation); + } + + decls.push(typeInfo); + } + // Enum Declarations + else if (node.type === "TSEnumDeclaration" && node.id?.name) { + const members = []; + + if (node.members) { + for (const member of node.members) { + if (member.id?.type === "Identifier") { + members.push(member.id.name); + } + } + } + decls.push({ name: node.id.name, - type: 'class', + type: "enum", doc_comment: docComment, start_line: loc.start, - end_line: loc.end - }); - } - else if (node.type === 'TSInterfaceDeclaration' && node.id?.name) { - const loc = getLoc(node); - decls.push({ - name: node.id.name, - type: 'interface', - doc_comment: docComment, - start_line: loc.start, - end_line: loc.end - }); - } - else if (node.type === 'TSTypeAliasDeclaration' && node.id?.name) { - const loc = getLoc(node); - decls.push({ - name: node.id.name, - type: 'type', - doc_comment: docComment, - start_line: loc.start, - end_line: loc.end + end_line: loc.end, + members: members, }); } + // Variable Declarations (const, let, var) else if ( - node.type === 'VariableDeclarator' && - node.id?.type === 'Identifier' + node.type === "VariableDeclarator" && + node.id?.type === "Identifier" ) { - const parent = node.parent; - if ( - parent?.type === 'VariableDeclaration' && - ['const', 'let'].includes(parent.kind) - ) { - const loc = getLoc(node); - decls.push({ + const parent = parentMap.get(node); + + if (parent?.type === "VariableDeclaration") { + const varInfo = { name: node.id.name, - type: 'variable', + type: "variable", doc_comment: docComment, start_line: loc.start, - end_line: loc.end - }); + end_line: loc.end, + var_kind: parent.kind, // const, let, or var + }; + + // Extract type annotation + if (node.id.typeAnnotation) { + varInfo.var_type = extractTypeString( + node.id.typeAnnotation.typeAnnotation, + ); + } + + decls.push(varInfo); + } + } + // Import Declarations + else if (node.type === "ImportDeclaration" && node.source?.value) { + const importInfo = { + name: node.source.value, + type: "import", + start_line: loc.start, + end_line: loc.end, + import_path: node.source.value, + }; + + // Extract imported names + const imports = []; + if (node.specifiers) { + for (const spec of node.specifiers) { + if (spec.type === "ImportSpecifier" && spec.imported?.name) { + imports.push(spec.imported.name); + } else if ( + spec.type === "ImportDefaultSpecifier" && + spec.local?.name + ) { + imports.push(`default as ${spec.local.name}`); + } else if ( + spec.type === "ImportNamespaceSpecifier" && + spec.local?.name + ) { + imports.push(`* as ${spec.local.name}`); + } + } + } + + if (imports.length > 0) { + importInfo.imported_names = imports; + } + + decls.push(importInfo); + } + // Export Declarations + else if (node.type === "ExportNamedDeclaration") { + if (node.declaration) { + // Export with declaration: export const x = ... + visit(node.declaration); + } else if (node.specifiers && node.specifiers.length > 0) { + // Named exports: export { x, y } + for (const spec of node.specifiers) { + if (spec.exported?.name) { + decls.push({ + name: spec.exported.name, + type: "export", + start_line: loc.start, + end_line: loc.end, + export_kind: "named", + }); + } + } } } + // Continue traversing Object.values(node).forEach(visit); } @@ -154,24 +475,29 @@ function extractDeclarations(ast, sourceCode) { return decls; } +// Main execution if (process.argv.length < 3) { - console.error('Usage: node parse_ts.js '); + console.error("Usage: node parse_ts.js "); process.exit(1); } const filePath = path.resolve(process.argv[2]); try { - const code = fs.readFileSync(filePath, 'utf8'); + const code = fs.readFileSync(filePath, "utf8"); const ast = parse(code, { - sourceType: 'module', + sourceType: "module", loc: true, range: true, - comment: false // we extract manually for simplicity + comment: false, + ecmaFeatures: { + jsx: true, + }, }); + const decls = extractDeclarations(ast, code); console.log(JSON.stringify(decls, null, 2)); } catch (e) { console.error(`ERR: ${e.message}`); process.exit(2); -} \ No newline at end of file +}