This commit is contained in:
2025-11-15 02:23:20 +00:00
parent 80e6be2120
commit 8cc9a3160e
2 changed files with 676 additions and 183 deletions
+259 -92
View File
@@ -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"<script(?:\s+[^>]*)?>(.*?)</script>", 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 <script ...> attributes
# Parse script blocks
script_matches = list(re.finditer(r"<script(?:\s+([^>]*?))?>(.*?)</script>", 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"<style(?:\s+[^>]*)?>(.*?)</style>", 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"<style(?:\s+([^>]*?))?>(.*?)</style>", 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"<script[^>]*>.*?</script>", "", text, flags=re.DOTALL)
markup = re.sub(r"<style[^>]*>.*?</style>", "", markup, flags=re.DOTALL).strip()
# Parse markup (template)
markup = text
# Remove script blocks
markup = re.sub(r"<script[^>]*>.*?</script>", "", markup, flags=re.DOTALL)
# Remove style blocks
markup = re.sub(r"<style[^>]*>.*?</style>", "", 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'<slot\s+name="([^"]+)"', markup)))
has_default_slot = '<slot' in markup and 'name=' not in markup[:markup.find('<slot') + 100]
# Extract stores used in template
template_stores = list(set(re.findall(r'\$([a-zA-Z_]\w*)', markup)))
# Extract directives
use_directives = list(set(re.findall(r'use:(\w+)', markup)))
transition_directives = list(set(re.findall(r'(?:transition|in|out):(\w+)', markup)))
# Extract bind directives
bind_directives = list(set(re.findall(r'bind:(\w+)', markup)))
# Extract reactive statements in template
reactive_statements = re.findall(r'\{([^}]*\$:\s*[^}]*)\}', markup)
chunk_text = f"File: {filepath}\nType: markup\nComponent: {component_name}\n"
if used_components:
chunk_text += f"Uses Components: {', '.join(set(used_components))}\n"
chunk_text += f"Uses Components: {', '.join(used_components)}\n"
if prop_bindings:
chunk_text += f"Prop Bindings: {', '.join([f'{prop}={val}' for prop, val in prop_bindings[:5]])}\n"
bindings_preview = [f"{prop}={{{val}}}" for prop, val in prop_bindings[:5]]
chunk_text += f"Prop Bindings: {', '.join(bindings_preview)}\n"
if event_handlers:
chunk_text += f"Event Handlers: {', '.join(set(event_handlers))}\n"
chunk_text += f"Event Handlers: {', '.join(event_handlers)}\n"
if slots or has_default_slot:
slot_info = []
if has_default_slot:
slot_info.append("default")
slot_info.extend(slots)
chunk_text += f"Slots: {', '.join(slot_info)}\n"
if template_stores:
chunk_text += f"Template Stores: {', '.join(template_stores)}\n"
if use_directives:
chunk_text += f"Use Directives: {', '.join(use_directives)}\n"
if transition_directives:
chunk_text += f"Transitions: {', '.join(transition_directives)}\n"
if bind_directives:
chunk_text += f"Bind Directives: {', '.join(bind_directives)}\n"
chunk_text += f"Markup:\n{markup}"
chunks.append({
@@ -1945,52 +2087,77 @@ def parse_svelte_file_cached(filepath_str: str, file_hash: str) -> 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."""