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
+}