"""JavaScript/TypeScript semantic parser using the TypeScript Compiler API.
This module replaces Tree-sitter's syntactic parsing with true semantic analysis
using the TypeScript compiler, enabling accurate type analysis, symbol resolution,
and cross-file understanding for JavaScript and TypeScript projects.
"""
import json
import os
import platform
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict, Optional, Any, List, Tuple
# Import our custom temp manager to avoid WSL2/Windows issues
try:
from theauditor.utils.temp_manager import TempManager
except ImportError:
# Fallback to regular tempfile if custom manager not available
TempManager = None
# Windows compatibility for subprocess calls
IS_WINDOWS = platform.system() == "Windows"
# Module-level cache for resolver (it's stateless now)
_module_resolver_cache = None
class JSSemanticParser:
"""Semantic parser for JavaScript/TypeScript using the TypeScript Compiler API."""
def __init__(self, project_root: str = None):
"""Initialize the semantic parser.
Args:
project_root: Absolute path to project root. If not provided, uses current directory.
"""
self.project_root = Path(project_root).resolve() if project_root else Path.cwd().resolve()
self.using_windows_node = False # Track if we're using Windows node.exe from WSL
self.tsc_path = None # Path to TypeScript compiler
self.node_modules_path = None # Path to sandbox node_modules
# CRITICAL: Reuse cached ModuleResolver (stateless, database-driven)
global _module_resolver_cache
if _module_resolver_cache is None:
from theauditor.module_resolver import ModuleResolver
_module_resolver_cache = ModuleResolver() # No project_root needed!
print("[DEBUG] Created singleton ModuleResolver instance")
self.module_resolver = _module_resolver_cache
# CRITICAL FIX: Find the sandboxed node executable (like linters do)
# Platform-agnostic: Check multiple possible locations
sandbox_base = self.project_root / ".auditor_venv" / ".theauditor_tools"
node_runtime = sandbox_base / "node-runtime"
# Check all possible node locations (Windows or Unix layout)
possible_node_paths = [
node_runtime / "node.exe", # Windows binary in root
node_runtime / "node", # Unix binary in root
node_runtime / "bin" / "node", # Unix binary in bin/
node_runtime / "bin" / "node.exe", # Windows binary in bin/ (unusual but possible)
]
self.node_exe = None
for node_path in possible_node_paths:
if node_path.exists():
self.node_exe = node_path
# Track if we're using Windows node on WSL
self.using_windows_node = str(node_path).endswith('.exe') and str(node_path).startswith('/')
break
# If not found, will trigger proper error messages
self.tsc_available = self._check_tsc_availability()
self.helper_script = self._create_helper_script()
self.batch_helper_script = self._create_batch_helper_script() # NEW: Batch processing helper
def _convert_path_for_node(self, path: Path) -> str:
"""Convert path to appropriate format for node execution.
If using Windows node.exe from WSL, converts to Windows path.
Otherwise returns the path as-is.
"""
path_str = str(path)
if self.using_windows_node:
try:
import subprocess as sp
result = sp.run(['wslpath', '-w', path_str],
capture_output=True, text=True, timeout=2)
if result.returncode == 0:
return result.stdout.strip()
except:
pass # Fall back to original path
return path_str
def _check_tsc_availability(self) -> bool:
"""Check if TypeScript compiler is available in our sandbox.
CRITICAL: We ONLY use our own sandboxed TypeScript installation.
We do not check or use any user-installed versions.
"""
# Check our sandbox location ONLY - no invasive checking of user's environment
# CRITICAL: Use absolute path from project root to avoid finding wrong sandboxes
sandbox_base = self.project_root / ".auditor_venv" / ".theauditor_tools" / "node_modules"
# Check if sandbox exists at the absolute location
sandbox_locations = [sandbox_base]
for sandbox_base in sandbox_locations:
if not sandbox_base.exists():
continue
# Check for TypeScript in sandbox
tsc_paths = [
sandbox_base / ".bin" / "tsc",
sandbox_base / ".bin" / "tsc.cmd", # Windows
]
# Also check for the actual TypeScript compiler JS file
tsc_js_path = sandbox_base / "typescript" / "lib" / "tsc.js"
# If we have node and the TypeScript compiler JS file, we can use it
if self.node_exe and tsc_js_path.exists():
try:
# Verify it actually works by running through node
# CRITICAL: Use absolute path for NODE_PATH
absolute_sandbox = sandbox_base.resolve()
# Use temp files to avoid buffer overflow
if TempManager:
stdout_path, stderr_path = TempManager.create_temp_files_for_subprocess(
str(self.project_root), "tsc_verify"
)
with open(stdout_path, 'w+', encoding='utf-8') as stdout_fp, \
open(stderr_path, 'w+', encoding='utf-8') as stderr_fp:
pass # File handles created, will be used below
else:
# Fallback to regular tempfile
with tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='_stdout.txt', encoding='utf-8') as stdout_fp, \
tempfile.NamedTemporaryFile(mode='w+', delete=False, suffix='_stderr.txt', encoding='utf-8') as stderr_fp:
stdout_path = stdout_fp.name
stderr_path = stderr_fp.name
with open(stdout_path, 'w+', encoding='utf-8') as stdout_fp, \
open(stderr_path, 'w+', encoding='utf-8') as stderr_fp:
# Convert paths for Windows node if needed
tsc_path_str = self._convert_path_for_node(tsc_js_path)
# Run TypeScript through node.exe
result = subprocess.run(
[str(self.node_exe), tsc_path_str, "--version"],
stdout=stdout_fp,
stderr=stderr_fp,
text=True,
timeout=5,
env={**os.environ, "NODE_PATH": str(absolute_sandbox)},
shell=False # Never use shell when we have full path
)
with open(stdout_path, 'r', encoding='utf-8') as f:
result.stdout = f.read()
with open(stderr_path, 'r', encoding='utf-8') as f:
result.stderr = f.read()
os.unlink(stdout_path)
os.unlink(stderr_path)
if result.returncode == 0:
self.tsc_path = tsc_js_path # Store the JS file path, not the shell script
self.node_modules_path = absolute_sandbox # Store absolute path
return True
except (subprocess.SubprocessError, FileNotFoundError, OSError):
pass # TypeScript check failed
# No sandbox TypeScript found - this is expected on first run
return False
def _extract_vue_blocks(self, content: str) -> Tuple[Optional[str], Optional[str]]:
"""Extract script and template blocks from Vue SFC content.
Args:
content: The raw Vue SFC file content
Returns:
Tuple of (script_content, template_content) or (None, None) if not found
"""
# Extract '
script_match = re.search(script_pattern, content, re.DOTALL | re.IGNORECASE)
script_content = script_match.group(1).strip() if script_match else None
# Extract block for future analysis
template_pattern = r']*)?>(.+?)'
template_match = re.search(template_pattern, content, re.DOTALL | re.IGNORECASE)
template_content = template_match.group(1).strip() if template_match else None
return script_content, template_content
def _create_helper_script(self) -> Path:
"""Create a Node.js helper script for TypeScript AST extraction.
Returns:
Path to the created helper script
"""
# CRITICAL: Create helper script with relative path resolution
# Always create in project root's .pf directory
pf_dir = self.project_root / ".pf"
pf_dir.mkdir(exist_ok=True)
helper_path = pf_dir / "tsc_ast_helper.js"
# Check if TypeScript module exists in our sandbox
typescript_exists = False
if self.node_modules_path:
# The TypeScript module is at node_modules/typescript/lib/typescript.js
ts_path = self.node_modules_path / "typescript" / "lib" / "typescript.js"
typescript_exists = ts_path.exists()
# Write the helper script that uses TypeScript Compiler API
# CRITICAL: Use relative path from helper script location to find TypeScript
helper_content = '''
// Use TypeScript from our sandbox location with RELATIVE PATH
// This is portable - works on any machine in any location
const path = require('path');
const fs = require('fs');
// Find project root by going up from .pf directory
const projectRoot = path.resolve(__dirname, '..');
// Build path to TypeScript module relative to project root
const tsPath = path.join(projectRoot, '.auditor_venv', '.theauditor_tools', 'node_modules', 'typescript', 'lib', 'typescript.js');
// Try to load TypeScript with helpful error message
let ts;
try {
if (!fs.existsSync(tsPath)) {
throw new Error(`TypeScript not found at expected location: ${tsPath}. Run 'aud setup-claude' to install tools.`);
}
ts = require(tsPath);
} catch (error) {
console.error(JSON.stringify({
success: false,
error: `Failed to load TypeScript: ${error.message}`,
expectedPath: tsPath,
projectRoot: projectRoot
}));
process.exit(1);
}
// Get file path and output path from command line arguments
const filePath = process.argv[2];
const outputPath = process.argv[3];
if (!filePath || !outputPath) {
console.error(JSON.stringify({ error: "File path and output path required" }));
process.exit(1);
}
try {
// Read the source file
const sourceCode = fs.readFileSync(filePath, 'utf8');
// Create a source file object
const sourceFile = ts.createSourceFile(
filePath,
sourceCode,
ts.ScriptTarget.Latest,
true, // setParentNodes - important for full AST traversal
ts.ScriptKind.TSX // Support both TS and TSX
);
// Helper function to serialize AST nodes
function serializeNode(node, depth = 0) {
if (depth > 100) { // Prevent infinite recursion
return { kind: "TooDeep" };
}
const result = {
kind: node.kind !== undefined ? (ts.SyntaxKind[node.kind] || node.kind) : 'Unknown',
kindValue: node.kind || 0,
pos: node.pos || 0,
end: node.end || 0,
flags: node.flags || 0
};
// Add text content for leaf nodes
if (node.text !== undefined) {
result.text = node.text;
}
// Add identifier name
if (node.name) {
if (typeof node.name === 'object') {
// Handle both escapedName and regular name
if (node.name.escapedText !== undefined) {
result.name = node.name.escapedText;
} else if (node.name.text !== undefined) {
result.name = node.name.text;
} else {
result.name = serializeNode(node.name, depth + 1);
}
} else {
result.name = node.name;
}
}
// Add type information if available
if (node.type) {
result.type = serializeNode(node.type, depth + 1);
}
// Add children - handle nodes with members property
const children = [];
if (node.members && Array.isArray(node.members)) {
// Handle nodes with members (interfaces, enums, etc.)
node.members.forEach(member => {
if (member) children.push(serializeNode(member, depth + 1));
});
}
ts.forEachChild(node, child => {
if (child) children.push(serializeNode(child, depth + 1));
});
if (children.length > 0) {
result.children = children;
}
// Get line and column information
// CRITICAL FIX: Use getStart() to exclude leading trivia for accurate line numbers
const actualStart = node.getStart ? node.getStart(sourceFile) : node.pos;
const { line, character } = sourceFile.getLineAndCharacterOfPosition(actualStart);
result.line = line + 1; // Convert to 1-indexed
result.column = character;
// RESTORED: Text extraction needed for accurate symbol names in taint analysis
result.text = sourceCode.substring(node.pos, node.end).trim();
return result;
}
// Collect diagnostics (errors, warnings)
const diagnostics = [];
const program = ts.createProgram([filePath], {
target: ts.ScriptTarget.Latest,
module: ts.ModuleKind.ESNext,
jsx: ts.JsxEmit.Preserve,
allowJs: true,
checkJs: false,
noEmit: true,
skipLibCheck: true // Skip checking .d.ts files for speed
});
const allDiagnostics = ts.getPreEmitDiagnostics(program);
allDiagnostics.forEach(diagnostic => {
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\\n');
const location = diagnostic.file && diagnostic.start
? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
: null;
diagnostics.push({
message,
category: ts.DiagnosticCategory[diagnostic.category],
code: diagnostic.code,
line: location ? location.line + 1 : null,
column: location ? location.character : null
});
});
// Collect symbols and type information
const checker = program.getTypeChecker();
const symbols = [];
// Visit nodes to collect symbols
function visit(node) {
try {
const symbol = checker.getSymbolAtLocation(node);
if (symbol && symbol.getName) {
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
const typeString = checker.typeToString(type);
symbols.push({
name: symbol.getName ? symbol.getName() : 'anonymous',
kind: symbol.flags ? (ts.SymbolFlags[symbol.flags] || symbol.flags) : 0,
type: typeString || 'unknown',
line: node.pos !== undefined ? sourceFile.getLineAndCharacterOfPosition(node.pos).line + 1 : 0
});
}
} catch (e) {
// Log error for debugging
console.error(`[ERROR] Symbol extraction failed at ${filePath}:${node.pos}: ${e.message}`);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
// Log symbol extraction results
console.error(`[INFO] Found ${symbols.length} symbols in ${filePath}`);
// Output the complete AST with metadata
const result = {
success: true,
fileName: filePath,
languageVersion: ts.ScriptTarget[sourceFile.languageVersion],
ast: serializeNode(sourceFile),
diagnostics: diagnostics,
symbols: symbols,
nodeCount: 0,
hasTypes: symbols.some(s => s.type && s.type !== 'any')
};
// Count nodes
function countNodes(node) {
if (!node) return;
result.nodeCount++;
if (node.children && Array.isArray(node.children)) {
node.children.forEach(countNodes);
}
}
if (result.ast) countNodes(result.ast);
// Write output to file instead of stdout to avoid pipe buffer limits
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2), 'utf8');
process.exit(0); // CRITICAL: Ensure clean exit on success
} catch (error) {
console.error(JSON.stringify({
success: false,
error: error.message,
stack: error.stack
}));
process.exit(1);
}
'''
helper_path.write_text(helper_content, encoding='utf-8')
return helper_path
def _create_batch_helper_script(self) -> Path:
"""Create a Node.js helper script for batch TypeScript AST extraction.
This script processes multiple files in a single TypeScript program,
dramatically improving performance by reusing the dependency cache.
Returns:
Path to the created batch helper script
"""
pf_dir = self.project_root / ".pf"
pf_dir.mkdir(exist_ok=True)
batch_helper_path = pf_dir / "tsc_batch_helper.js"
batch_helper_content = '''
// Batch TypeScript AST extraction - processes multiple files in one program
const path = require('path');
const fs = require('fs');
// Find project root by going up from .pf directory
const projectRoot = path.resolve(__dirname, '..');
// Build path to TypeScript module
const tsPath = path.join(projectRoot, '.auditor_venv', '.theauditor_tools', 'node_modules', 'typescript', 'lib', 'typescript.js');
// Load TypeScript
let ts;
try {
if (!fs.existsSync(tsPath)) {
throw new Error(`TypeScript not found at: ${tsPath}`);
}
ts = require(tsPath);
} catch (error) {
console.error(JSON.stringify({
success: false,
error: `Failed to load TypeScript: ${error.message}`
}));
process.exit(1);
}
// Get request and output paths from command line
const requestPath = process.argv[2];
const outputPath = process.argv[3];
if (!requestPath || !outputPath) {
console.error(JSON.stringify({ error: "Request and output paths required" }));
process.exit(1);
}
try {
// Read batch request
const request = JSON.parse(fs.readFileSync(requestPath, 'utf8'));
const filePaths = request.files || [];
if (filePaths.length === 0) {
fs.writeFileSync(outputPath, JSON.stringify({}), 'utf8');
process.exit(0);
}
// Create a SINGLE TypeScript program with ALL files
// This is the key optimization - TypeScript will parse dependencies ONCE
const program = ts.createProgram(filePaths, {
target: ts.ScriptTarget.Latest,
module: ts.ModuleKind.ESNext,
jsx: ts.JsxEmit.Preserve,
allowJs: true,
checkJs: false,
noEmit: true,
skipLibCheck: true, // Skip checking .d.ts files for speed
moduleResolution: ts.ModuleResolutionKind.NodeJs
});
const checker = program.getTypeChecker();
const results = {};
// Process each file using the SHARED program
for (const filePath of filePaths) {
try {
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
results[filePath] = {
success: false,
error: `Could not load source file: ${filePath}`
};
continue;
}
const sourceCode = sourceFile.text;
// Helper function to serialize AST nodes (same as single-file version)
function serializeNode(node, depth = 0) {
if (depth > 100) return { kind: "TooDeep" };
const result = {
kind: node.kind !== undefined ? (ts.SyntaxKind[node.kind] || node.kind) : 'Unknown',
kindValue: node.kind || 0,
pos: node.pos || 0,
end: node.end || 0,
flags: node.flags || 0
};
if (node.text !== undefined) result.text = node.text;
if (node.name) {
if (typeof node.name === 'object') {
if (node.name.escapedText !== undefined) {
result.name = node.name.escapedText;
} else if (node.name.text !== undefined) {
result.name = node.name.text;
} else {
result.name = serializeNode(node.name, depth + 1);
}
} else {
result.name = node.name;
}
}
if (node.type) {
result.type = serializeNode(node.type, depth + 1);
}
const children = [];
if (node.members && Array.isArray(node.members)) {
node.members.forEach(member => {
if (member) children.push(serializeNode(member, depth + 1));
});
}
ts.forEachChild(node, child => {
if (child) children.push(serializeNode(child, depth + 1));
});
if (children.length > 0) {
result.children = children;
}
// CRITICAL FIX: Use getStart() to exclude leading trivia for accurate line numbers
const actualStart = node.getStart ? node.getStart(sourceFile) : node.pos;
const { line, character } = sourceFile.getLineAndCharacterOfPosition(actualStart);
result.line = line + 1;
result.column = character;
// RESTORED: Text extraction needed for accurate symbol names in taint analysis
result.text = sourceCode.substring(node.pos, node.end).trim();
return result;
}
// Collect diagnostics for this file
const diagnostics = [];
const fileDiagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
fileDiagnostics.forEach(diagnostic => {
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\\n');
const location = diagnostic.file && diagnostic.start
? diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
: null;
diagnostics.push({
message,
category: ts.DiagnosticCategory[diagnostic.category],
code: diagnostic.code,
line: location ? location.line + 1 : null,
column: location ? location.character : null
});
});
// Collect symbols for this file
const symbols = [];
function visit(node) {
try {
const symbol = checker.getSymbolAtLocation(node);
if (symbol && symbol.getName) {
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
const typeString = checker.typeToString(type);
symbols.push({
name: symbol.getName ? symbol.getName() : 'anonymous',
kind: symbol.flags ? (ts.SymbolFlags[symbol.flags] || symbol.flags) : 0,
type: typeString || 'unknown',
line: node.pos !== undefined ? sourceFile.getLineAndCharacterOfPosition(node.pos).line + 1 : 0
});
}
} catch (e) {
// Log error for debugging
console.error(`[ERROR] Symbol extraction failed at ${filePath}:${node.pos}: ${e.message}`);
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
// Log symbol extraction results
console.error(`[INFO] Found ${symbols.length} symbols in ${filePath}`);
// Build result for this file
const result = {
success: true,
fileName: filePath,
languageVersion: ts.ScriptTarget[sourceFile.languageVersion],
ast: serializeNode(sourceFile),
diagnostics: diagnostics,
symbols: symbols,
nodeCount: 0,
hasTypes: symbols.some(s => s.type && s.type !== 'any')
};
// Count nodes
function countNodes(node) {
if (!node) return;
result.nodeCount++;
if (node.children && Array.isArray(node.children)) {
node.children.forEach(countNodes);
}
}
if (result.ast) countNodes(result.ast);
results[filePath] = result;
} catch (error) {
results[filePath] = {
success: false,
error: `Error processing file: ${error.message}`,
ast: null,
diagnostics: [],
symbols: []
};
}
}
// Write all results to output file
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf8');
process.exit(0);
} catch (error) {
console.error(JSON.stringify({
success: false,
error: error.message,
stack: error.stack
}));
process.exit(1);
}
'''
batch_helper_path.write_text(batch_helper_content, encoding='utf-8')
return batch_helper_path
def get_semantic_ast_batch(self, file_paths: List[str]) -> Dict[str, Dict[str, Any]]:
"""Get semantic ASTs for multiple JavaScript/TypeScript files in a single process.
This dramatically improves performance by reusing the TypeScript program
and dependency cache across multiple files.
Args:
file_paths: List of paths to JavaScript or TypeScript files to parse
Returns:
Dictionary mapping file paths to their AST results
"""
# Validate all files exist
results = {}
valid_files = []
for file_path in file_paths:
file = Path(file_path).resolve()
if not file.exists():
results[file_path] = {
"success": False,
"error": f"File not found: {file_path}",
"ast": None,
"diagnostics": [],
"symbols": []
}
elif file.suffix.lower() not in ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue']:
results[file_path] = {
"success": False,
"error": f"Not a JavaScript/TypeScript file: {file_path}",
"ast": None,
"diagnostics": [],
"symbols": []
}
else:
valid_files.append(str(file.resolve()))
if not valid_files:
return results
if not self.tsc_available:
for file_path in valid_files:
results[file_path] = {
"success": False,
"error": "TypeScript compiler not available in TheAuditor sandbox. Run 'aud setup-claude' to install tools.",
"ast": None,
"diagnostics": [],
"symbols": []
}
return results
try:
# Create batch request
batch_request = {
"files": valid_files,
"projectRoot": str(self.project_root)
}
# Write batch request to temp file
if TempManager:
request_path, req_fd = TempManager.create_temp_file(str(self.project_root), suffix='_request.json')
os.close(req_fd)
output_path, out_fd = TempManager.create_temp_file(str(self.project_root), suffix='_output.json')
os.close(out_fd)
else:
# Fallback to regular tempfile
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as tmp_req:
request_path = tmp_req.name
with tempfile.NamedTemporaryFile(mode='w+', suffix='.json', delete=False, encoding='utf-8') as tmp_out:
output_path = tmp_out.name
# Write batch request data
with open(request_path, 'w', encoding='utf-8') as f:
json.dump(batch_request, f)
# Calculate timeout based on batch size
# 5 seconds base + 2 seconds per file
dynamic_timeout = min(5 + (len(valid_files) * 2), 120)
try:
# Run batch helper script
# Convert paths for Windows node if needed
helper_path = self._convert_path_for_node(self.batch_helper_script.resolve())
request_path_converted = self._convert_path_for_node(Path(request_path))
output_path_converted = self._convert_path_for_node(Path(output_path))
# CRITICAL FIX: Use sandboxed node executable, not system "node"
if not self.node_exe:
raise RuntimeError("Node.js runtime not found. Run 'aud setup-claude' to install tools.")
result = subprocess.run(
[str(self.node_exe), helper_path, request_path_converted, output_path_converted],
capture_output=False,
stderr=subprocess.PIPE,
text=True,
timeout=dynamic_timeout,
cwd=self.project_root,
shell=IS_WINDOWS # Windows compatibility fix
)
if result.returncode != 0:
error_msg = f"Batch TypeScript compiler failed (exit code {result.returncode})"
if result.stderr:
error_msg += f": {result.stderr.strip()[:500]}"
for file_path in valid_files:
results[file_path] = {
"success": False,
"error": error_msg,
"ast": None,
"diagnostics": [],
"symbols": []
}
else:
# Read batch results
if Path(output_path).exists():
with open(output_path, 'r', encoding='utf-8') as f:
batch_results = json.load(f)
# Map results back to original file paths
for file_path in file_paths:
resolved_path = str(Path(file_path).resolve())
if resolved_path in batch_results:
results[file_path] = batch_results[resolved_path]
elif file_path not in results:
results[file_path] = {
"success": False,
"error": "File not processed in batch",
"ast": None,
"diagnostics": [],
"symbols": []
}
else:
for file_path in valid_files:
results[file_path] = {
"success": False,
"error": "Batch output file not created",
"ast": None,
"diagnostics": [],
"symbols": []
}
finally:
# Clean up temp files
for temp_path in [request_path, output_path]:
if Path(temp_path).exists():
Path(temp_path).unlink()
except subprocess.TimeoutExpired:
for file_path in valid_files:
results[file_path] = {
"success": False,
"error": f"Batch timeout: Files too large or complex to parse within {dynamic_timeout:.0f} seconds",
"ast": None,
"diagnostics": [],
"symbols": []
}
except Exception as e:
for file_path in valid_files:
results[file_path] = {
"success": False,
"error": f"Unexpected error in batch processing: {e}",
"ast": None,
"diagnostics": [],
"symbols": []
}
return results
def get_semantic_ast(self, file_path: str) -> Dict[str, Any]:
"""Get semantic AST for a JavaScript/TypeScript file using the TypeScript compiler.
Args:
file_path: Path to the JavaScript or TypeScript file to parse
Returns:
Dictionary containing the semantic AST and metadata:
- success: Boolean indicating if parsing was successful
- ast: The full AST tree with semantic information
- diagnostics: List of errors/warnings from TypeScript
- symbols: List of symbols with type information
- nodeCount: Total number of AST nodes
- hasTypes: Boolean indicating if type information is available
- error: Error message if parsing failed
"""
# Validate file exists
file = Path(file_path).resolve()
if not file.exists():
return {
"success": False,
"error": f"File not found: {file_path}",
"ast": None,
"diagnostics": [],
"symbols": []
}
# Check if it's a JavaScript, TypeScript, or Vue file
if file.suffix.lower() not in ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue']:
return {
"success": False,
"error": f"Not a JavaScript/TypeScript file: {file_path}",
"ast": None,
"diagnostics": [],
"symbols": []
}
# CRITICAL: No fallbacks allowed - fail fast with clear error
if not self.tsc_available:
return {
"success": False,
"error": "TypeScript compiler not available in TheAuditor sandbox. Run 'aud setup-claude' to install tools.",
"ast": None,
"diagnostics": [],
"symbols": []
}
try:
# CRITICAL: No automatic installation - user must install TypeScript manually
# This enforces fail-fast philosophy
# Handle Vue SFC files specially
actual_file_to_parse = file_path
vue_metadata = None
temp_file = None
if file.suffix.lower() == '.vue':
# Read Vue SFC content
vue_content = file.read_text(encoding='utf-8')
script_content, template_content = self._extract_vue_blocks(vue_content)
if script_content is None:
return {
"success": False,
"error": "No