import { parse as acornParse } from '../../vendor/acorn/dist/acorn.mjs'
const PARSE_OPTS = {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
ranges: true,
allowHashBang: true
}
const CJS_PARSE_OPTS = {
...PARSE_OPTS,
sourceType: 'script'
}
export function parseFile (source, filePath) {
const comments = []
const isESM = !filePath || !filePath.endsWith('.cjs')
const opts = {
...(isESM ? PARSE_OPTS : CJS_PARSE_OPTS),
onComment: comments
}
let ast
try {
ast = acornParse(source, opts)
} catch (err) {
// Fall back to script mode if module parse fails (CJS files)
try {
ast = acornParse(source, { ...CJS_PARSE_OPTS, onComment: comments })
} catch {
return { error: err.message, filePath }
}
}
const result = {
filePath,
imports: [],
exports: [],
classes: [],
functions: [],
constants: [],
state: [],
errorContracts: [],
delegates: []
}
const commentMap = buildCommentMap(comments)
for (const node of ast.body) {
switch (node.type) {
case 'ImportDeclaration':
result.imports.push(extractImport(node))
break
case 'ExportNamedDeclaration':
extractNamedExport(node, result, commentMap)
break
case 'ExportDefaultDeclaration':
extractDefaultExport(node, result, commentMap)
break
case 'ExportAllDeclaration':
result.exports.push({
name: '*',
from: node.source ? node.source.value : null
})
break
case 'ClassDeclaration':
if (node.id) {
result.classes.push(extractClass(node, commentMap))
}
break
case 'FunctionDeclaration':
if (node.id) {
result.functions.push(extractFunction(node, commentMap))
}
break
case 'VariableDeclaration':
extractConstants(node, result, commentMap)
break
// CJS: module.exports = ...
case 'ExpressionStatement':
extractCJSExport(node, result, commentMap)
break
}
}
// Walk full AST for state, errors, delegation
walkDeep(ast, result)
return result
}
// --- Comment mapping ---
function buildCommentMap (comments) {
// Map: endLine -> comment text (for block comments / JSDoc)
const map = []
for (const c of comments) {
if (c.type === 'Block' && c.value.startsWith('*')) {
map.push({
text: '/*' + c.value + '*/',
endLine: c.loc.end.line,
start: c.start,
end: c.end
})
}
}
return map
}
function findJSDoc (commentMap, node) {
if (!node.loc) return null
const nodeLine = node.loc.start.line
for (const c of commentMap) {
// JSDoc comment ends on the line before or same line as the node
if (c.endLine >= nodeLine - 2 && c.endLine < nodeLine) {
return c.text
}
}
return null
}
// --- Import extraction ---
function extractImport (node) {
const result = {
specifier: node.source.value,
names: []
}
for (const spec of node.specifiers) {
switch (spec.type) {
case 'ImportSpecifier':
result.names.push(spec.imported.name)
break
case 'ImportDefaultSpecifier':
result.names.push(spec.local.name)
break
case 'ImportNamespaceSpecifier':
result.names.push('* as ' + spec.local.name)
break
}
}
return result
}
// --- Export extraction ---
function extractNamedExport (node, result, commentMap) {
// Re-export: export { X } from './file.js'
//
// This is sugar for import-then-export, so it is BOTH. Recording only the
// export leaves the source file with nothing pointing at it: a pure re-export
// barrel (index.js doing nothing but `export { x } from './lib/x.js'`) then
// makes every file it fronts look unreachable.
if (node.source) {
const names = []
for (const spec of node.specifiers) {
const name = spec.exported.name || spec.exported.value
names.push(spec.local ? (spec.local.name || name) : name)
result.exports.push({ name, from: node.source.value })
}
if (names.length > 0) {
result.imports.push({ specifier: node.source.value, names, reExport: true })
}
return
}
// export class X { ... }
if (node.declaration) {
const decl = node.declaration
switch (decl.type) {
case 'ClassDeclaration': {
const cls = extractClass(decl, commentMap)
result.classes.push(cls)
result.exports.push({ name: cls.name, type: 'class' })
break
}
case 'FunctionDeclaration': {
const fn = extractFunction(decl, commentMap)
result.functions.push(fn)
result.exports.push({ name: fn.name, type: 'function' })
break
}
case 'VariableDeclaration': {
extractConstants(decl, result, commentMap)
for (const d of decl.declarations) {
if (d.id && d.id.name) {
result.exports.push({ name: d.id.name, type: 'const' })
}
}
break
}
}
return
}
// export { X, Y }
for (const spec of node.specifiers) {
result.exports.push({
name: spec.exported.name || spec.exported.value
})
}
}
function extractDefaultExport (node, result, commentMap) {
const decl = node.declaration
if (decl.type === 'ClassDeclaration' && decl.id) {
const cls = extractClass(decl, commentMap)
result.classes.push(cls)
result.exports.push({ name: 'default', type: 'class', localName: cls.name })
} else if (decl.type === 'FunctionDeclaration' && decl.id) {
const fn = extractFunction(decl, commentMap)
result.functions.push(fn)
result.exports.push({ name: 'default', type: 'function', localName: fn.name })
} else {
result.exports.push({ name: 'default', type: decl.type })
}
}
function extractCJSExport (node, result, commentMap) {
const expr = node.expression
if (!expr || expr.type !== 'AssignmentExpression') return
const left = expr.left
if (!left || left.type !== 'MemberExpression') return
// module.exports = class X { ... }
const isModuleExports = left.object &&
left.object.name === 'module' &&
left.property && left.property.name === 'exports'
// exports.X = ...
const isExportsProp = left.object &&
left.object.name === 'exports' &&
left.property
if (isModuleExports) {
const right = expr.right
if (right.type === 'ClassExpression' || right.type === 'ClassDeclaration') {
const cls = extractClass(right, commentMap)
result.classes.push(cls)
result.exports.push({ name: 'default', type: 'class', localName: cls.name, cjs: true })
} else if (right.type === 'FunctionExpression' || right.type === 'FunctionDeclaration') {
const fn = extractFunction(right, commentMap)
result.functions.push(fn)
result.exports.push({ name: 'default', type: 'function', localName: fn.name, cjs: true })
} else {
result.exports.push({ name: 'default', type: right.type, cjs: true })
}
} else if (isExportsProp) {
const name = left.property.name || left.property.value
result.exports.push({ name, cjs: true })
}
}
// --- Class extraction ---
function extractClass (node, commentMap) {
const cls = {
name: node.id ? node.id.name : '(anonymous)',
extends: node.superClass ? nodeToString(node.superClass) : null,
line: node.loc ? node.loc.start.line : null,
endLine: node.loc ? node.loc.end.line : null,
constructor: null,
methods: [],
getters: [],
staticMethods: [],
jsdoc: findJSDoc(commentMap, node)
}
if (!node.body || !node.body.body) return cls
for (const member of node.body.body) {
if (member.type === 'MethodDefinition') {
const name = member.key.name || member.key.value || '(computed)'
const jsdoc = findJSDoc(commentMap, member)
if (member.kind === 'constructor') {
cls.constructor = {
params: extractParams(member.value.params),
line: member.loc ? member.loc.start.line : null,
endLine: member.loc ? member.loc.end.line : null,
jsdoc
}
} else if (member.kind === 'get') {
cls.getters.push({ name, static: member.static || false, line: member.loc ? member.loc.start.line : null, endLine: member.loc ? member.loc.end.line : null })
} else if (member.kind === 'set') {
cls.getters.push({ name, kind: 'set', static: member.static || false })
} else if (member.static) {
cls.staticMethods.push({
name,
params: extractParams(member.value.params),
async: member.value.async || false,
line: member.loc ? member.loc.start.line : null,
endLine: member.loc ? member.loc.end.line : null,
jsdoc
})
} else {
cls.methods.push({
name,
params: extractParams(member.value.params),
async: member.value.async || false,
generator: member.value.generator || false,
line: member.loc ? member.loc.start.line : null,
endLine: member.loc ? member.loc.end.line : null,
jsdoc
})
}
} else if (member.type === 'PropertyDefinition') {
// Class fields (public or private)
const name = member.key.name || member.key.value || '(computed)'
cls.methods.push({
name,
kind: 'field',
static: member.static || false
})
}
}
return cls
}
// --- Function extraction ---
function extractFunction (node, commentMap) {
return {
name: node.id ? node.id.name : '(anonymous)',
params: extractParams(node.params),
async: node.async || false,
generator: node.generator || false,
line: node.loc ? node.loc.start.line : null,
endLine: node.loc ? node.loc.end.line : null,
jsdoc: findJSDoc(commentMap, node)
}
}
// --- Constants ---
function extractConstants (node, result, commentMap) {
for (const decl of node.declarations) {
if (!decl.id) continue
if (decl.id.type === 'Identifier') {
const entry = {
name: decl.id.name,
kind: node.kind,
jsdoc: findJSDoc(commentMap, node)
}
// If the init is a class or function, extract it
if (decl.init) {
if (decl.init.type === 'ClassExpression') {
const cls = extractClass(decl.init, commentMap)
cls.name = cls.name === '(anonymous)' ? decl.id.name : cls.name
result.classes.push(cls)
entry.type = 'class'
} else if (decl.init.type === 'ArrowFunctionExpression' || decl.init.type === 'FunctionExpression') {
result.functions.push({
name: decl.id.name,
params: extractParams(decl.init.params),
async: decl.init.async || false,
generator: decl.init.generator || false,
arrow: decl.init.type === 'ArrowFunctionExpression',
jsdoc: findJSDoc(commentMap, node)
})
entry.type = 'function'
}
}
result.constants.push(entry)
}
}
}
// --- Deep walk for state, errors, delegation ---
function walkDeep (ast, result) {
const stateFields = new Set()
const delegateCalls = new Map() // field -> Set of methods
const errors = []
walk(ast, (node) => {
// Dynamic import. The top-level switch only sees static ImportDeclaration,
// so a module reached solely via `await import('x')` looked like nothing
// imported it — and every one of its exports read as a dead export.
//
// const { startServer } = await import('./lib/server.js')
//
// Visited parent-first, so the destructured form is recorded (with its
// names) before the bare ImportExpression fallback below sees it.
if (
node.type === 'VariableDeclarator' &&
node.id && node.id.type === 'ObjectPattern' &&
node.init && node.init.type === 'AwaitExpression' &&
node.init.argument && node.init.argument.type === 'ImportExpression' &&
node.init.argument.source && node.init.argument.source.type === 'Literal'
) {
const spec = node.init.argument.source.value
if (typeof spec === 'string' && !result.imports.some(i => i.specifier === spec)) {
const names = []
for (const p of node.id.properties) {
if (p.type === 'Property' && p.key && p.key.name) names.push(p.key.name)
}
result.imports.push({ specifier: spec, names, dynamic: true })
}
}
// Any other dynamic import — bare `import('x')`, or one whose result is not
// destructured. The edge still matters even when no names are recoverable.
if (node.type === 'ImportExpression' && node.source && node.source.type === 'Literal') {
const spec = node.source.value
if (typeof spec === 'string' && !result.imports.some(i => i.specifier === spec)) {
result.imports.push({ specifier: spec, names: [], dynamic: true })
}
}
// this._field = ... (in constructor or methods)
if (
node.type === 'AssignmentExpression' &&
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
node.left.property.type === 'Identifier' &&
node.left.property.name.startsWith('_')
) {
stateFields.add(node.left.property.name)
}
// throw new Error('...')
if (
node.type === 'ThrowStatement' &&
node.argument &&
node.argument.type === 'NewExpression' &&
node.argument.callee &&
node.argument.callee.name === 'Error' &&
node.argument.arguments.length > 0
) {
const arg = node.argument.arguments[0]
if (arg.type === 'Literal') {
errors.push(arg.value)
} else if (arg.type === 'TemplateLiteral') {
errors.push(templateToPattern(arg))
}
}
// this._field.method() — delegation
if (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'MemberExpression' &&
node.callee.object.object.type === 'ThisExpression' &&
node.callee.object.property.type === 'Identifier' &&
node.callee.object.property.name.startsWith('_') &&
node.callee.property.type === 'Identifier'
) {
const field = node.callee.object.property.name
const method = node.callee.property.name
if (!delegateCalls.has(field)) delegateCalls.set(field, new Set())
delegateCalls.get(field).add(method)
}
})
result.state = Array.from(stateFields)
result.errorContracts = errors
for (const [field, methods] of delegateCalls) {
result.delegates.push({
field,
methods: Array.from(methods)
})
}
}
// --- Helpers ---
function extractParams (params) {
return params.map(paramToString)
}
function paramToString (param) {
switch (param.type) {
case 'Identifier':
return param.name
case 'AssignmentPattern':
return paramToString(param.left)
case 'RestElement':
return '...' + paramToString(param.argument)
case 'ObjectPattern':
return '{ ' + param.properties.map(p => {
if (p.type === 'RestElement') return '...' + paramToString(p.argument)
return p.key.name || p.key.value || '(computed)'
}).join(', ') + ' }'
case 'ArrayPattern':
return '[' + param.elements.map(e => e ? paramToString(e) : '').join(', ') + ']'
default:
return '(?)'
}
}
function nodeToString (node) {
if (node.type === 'Identifier') return node.name
if (node.type === 'MemberExpression') {
return nodeToString(node.object) + '.' + (node.property.name || node.property.value)
}
return '(?)'
}
function templateToPattern (node) {
let out = ''
for (let i = 0; i < node.quasis.length; i++) {
out += node.quasis[i].value.raw
if (i < node.expressions.length) {
out += '${...}'
}
}
return out
}
function walk (node, fn) {
if (!node || typeof node !== 'object') return
fn(node)
for (const key of Object.keys(node)) {
const val = node[key]
if (Array.isArray(val)) {
for (const child of val) {
if (child && typeof child.type === 'string') walk(child, fn)
}
} else if (val && typeof val.type === 'string') {
walk(val, fn)
}
}
}