2import * as _ from 'lodash-es' 4import ts from 'typescript' 15type TsNode = ts.Node & {[k: string]: any} 17export const getModuleDefNameH = (contents: string, { ptPath }: {ptPath: string}) => { 20 const exportA: string[] = [] 21 const privateA: string[] = [] 22 const referencedA = new Set<string>() 23 const allLocalDeclarations = new Set<string>() 24 const stringLiteralValuesA: string[] = [] 25 const templateLiteralContentsA: string[] = [] 26 const propertyAccessNamesA: string[] = [] 29 const sourceFile = ts.createSourceFile(ptPath, contents, ts.ScriptTarget.Latest, true) 31 const visit = (node: TsNode, parent: TsNode | null) => { 32 if (ts.isVariableStatement(node)) { 33 const isExport = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) 34 const isTopLevel = parent === sourceFile 35 node.declarationList.declarations.forEach(decl => { 36 if (ts.isIdentifier(decl.name)) { 37 allLocalDeclarations.add(decl.name.text) 38 if (isTopLevel) (isExport ? exportA : privateA).push(decl.name.text) 41 } else if (ts.isFunctionDeclaration(node) && node.name) { 42 const isExport = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) 43 const isTopLevel = parent === sourceFile 44 allLocalDeclarations.add(node.name.text) 45 if (isTopLevel) (isExport ? exportA : privateA).push(node.name.text) 47 if (ts.isParameter(node) && ts.isIdentifier(node.name)) { 48 allLocalDeclarations.add(node.name.text) 51 if (ts.isIdentifier(node) && parent) { 52 const isDeclarationDefiningPosition = 53 ((ts.isFunctionDeclaration(parent) || ts.isClassDeclaration(parent)) && parent.name === node) || 54 (ts.isVariableDeclaration(parent) && parent.name === node) || 55 (ts.isParameter(parent) && parent.name === node) || 56 (ts.isImportSpecifier(parent) && parent.propertyName !== node) || 57 (ts.isPropertyAccessExpression(parent) && parent.name === node) || 58 (ts.isPropertyAssignment(parent) && parent.name === node); 60 if (!isDeclarationDefiningPosition) referencedA.add(node.text) 61 if (ts.isPropertyAccessExpression(parent) && parent.name === node) propertyAccessNamesA.push(node.text) 63 if (ts.isStringLiteral(node)) stringLiteralValuesA.push(node.text) 64 if (ts.isNoSubstitutionTemplateLiteral(node)) templateLiteralContentsA.push(node.text) 65 if (ts.isTemplateExpression(node)) { 66 templateLiteralContentsA.push(node.head.text + node.templateSpans.map(s => s.literal.text).join('')) 69 ts.forEachChild(node, (child) => visit(child, node)) 72 visit(sourceFile, null) 77 // Single pass: collect top-level exports and privates 78 for (const node of (ast as AcornProgram).body as AcornNode[]) { 79 if (node.type === 'ExportNamedDeclaration') { 80 if (node.declaration) { 81 if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) { 82 exportA.push(node.declaration.id.name) 83 } else if (node.declaration.type === 'VariableDeclaration') { 84 for (const decl of node.declaration.declarations) { 85 if (decl.id?.type === 'Identifier') exportA.push(decl.id.name) 88 } else if (node.specifiers) { 89 for (const spec of node.specifiers as AcornNode[]) exportA.push(spec.local.name) 91 } else if (node.type === 'VariableDeclaration') { 92 for (const decl of node.declarations) { 93 if (decl.id.type === 'Identifier') privateA.push(decl.id.name) 95 } else if (node.type === 'FunctionDeclaration' && node.id) { 96 privateA.push(node.id.name) 100 const findReferences = (node: AcornNode, parent: AcornNode | null) => { 101 if (!node || typeof node !== 'object') return; 103 if (node.type === 'VariableDeclaration') { 104 for (const decl of node.declarations) { 105 if (decl.id?.type === 'Identifier') allLocalDeclarations.add(decl.id.name) 107 } else if (node.type === 'FunctionDeclaration' && node.id) { 108 allLocalDeclarations.add(node.id.name) 109 } else if (node.type === 'ClassDeclaration' && node.id) { 110 allLocalDeclarations.add(node.id.name) 112 if ((node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') && node.params) { 113 for (const param of node.params) { 114 if (param.type === 'Identifier') allLocalDeclarations.add(param.name) 118 if (node.type === 'JSXIdentifier' && node.name && /^[A-Z]/.test(node.name) && parent?.type === 'JSXOpeningElement') { 119 referencedA.add(node.name) 122 if (node.type === 'Identifier') { 123 const isDecl = parent && ( 124 ((parent.type === 'FunctionDeclaration' || parent.type === 'VariableDeclarator') && parent.id === node) || 125 (parent.type === 'MemberExpression' && parent.property === node && !parent.computed) || 126 (parent.type === 'Property' && parent.key === node && !parent.computed) || 127 ((parent.type === 'FunctionDeclaration' || parent.type === 'ArrowFunctionExpression') && parent.params.includes(node)) || 128 ((parent.type === 'ImportSpecifier' || parent.type === 'ImportDefaultSpecifier') && parent.local === node) 130 if (!isDecl) referencedA.add(node.name) 131 if (parent?.type === 'MemberExpression' && parent.property === node && !parent.computed) propertyAccessNamesA.push(node.name) 133 if (node.type === 'Literal' && typeof node.value === 'string') stringLiteralValuesA.push(node.value) 134 if (node.type === 'TemplateLiteral' && node.quasis) { 135 templateLiteralContentsA.push(node.quasis.map((q: AcornNode) => q.value?.raw || '').join('')) 138 for (const key in node) { 139 if (Object.prototype.hasOwnProperty.call(node, key)) { 140 const child = node[key]; 141 if (Array.isArray(child)) child.forEach(item => findReferences(item, node)); 142 else if (child && typeof child === 'object') findReferences(child, node); 146 findReferences(ast, null); 149 const referencedAArr = [...referencedA] 150 const externalRefsA = _.difference(referencedAArr, [...allLocalDeclarations]) 152 return { privateA, exportA, referencedA: referencedAArr, externalRefsA, stringLiteralValuesA, templateLiteralContentsA, propertyAccessNamesA } 155const getFileExportDataCached = async (ptPath: string) => { 157 const mtime = fs.statSync(absPath, {throwIfNoEntry: false})?.mtimeMs || 0 158 return runMemoTempfile({cacheKeyA: ['fileExportData-v1', ptPath, String(mtime)]}, async () => { 160 if (contents.includes('//pt' + 'noshake')) return null 161 return getModuleDefNameH(contents, { ptPath }) 165const extractImportNamesFromTemplateLiteral = (templateContent: string): string[] => { 167 const ast = acornParse({contents: templateContent, ptPath: 'template-literal' as unknown as absFileDirPath}) 168 const names: string[] = [] 169 for (const node of (ast as AcornProgram).body as AcornNode[]) { 170 if (node.type !== 'ImportDeclaration') continue 171 for (const spec of node.specifiers) { 172 if (spec.type === 'ImportSpecifier') names.push(spec.imported?.name ?? spec.local.name) 173 else if (spec.type === 'ImportDefaultSpecifier') names.push(spec.local.name) 177 } catch { return [] } 180const collectFileExportData = async (pathsA: absFileDirPath[]) => { 183 if (++visited % 500 === 0) resetAcornParseCount() // sanctioned repo-wide bulk walk — exempt from OOM breaker (same pattern as ptMadge) 185 const empty = { path: filePath, exports: [] as string[], internalRefs: [] as string[], imports: {} as Record<string, string[]>, 186 stringLiteralValues: [] as string[], templateLiteralContents: [] as string[], propertyAccessNames: [] as string[] } 187 const modData = await getFileExportDataCached(ptPath) 188 if (!modData) return empty 191 return { path: filePath, exports: modData.exportA, internalRefs: modData.referencedA, imports, 192 stringLiteralValues: modData.stringLiteralValuesA, templateLiteralContents: modData.templateLiteralContentsA, propertyAccessNames: modData.propertyAccessNamesA } 195 const starImportedFiles = new Set<string>() 196 const allImportedNames = new Set<string>() 197 for (const data of fileData) { 198 for (const [importedFile, names] of Object.entries(data.imports)) { 199 if ((names as string[]).includes('*')) starImportedFiles.add(importedFile) 200 for (const name of names as string[]) allImportedNames.add(name) 203 for (const fd of fileData) { 205 for (const name of fd.exports) allImportedNames.add(name) 209 // Detect exports consumed via dynamic dispatch (AST-based): 210 // string literals (renderJsxToHtml), property access (requireSync/module1ToObj), template literal imports (<script type="module">) 211 const allExportNames = new Set(fileData.flatMap(d => d.exports)) 212 const allStringLiterals = new Set(fileData.flatMap(d => d.stringLiteralValues)) 213 const allPropertyAccess = new Set(fileData.flatMap(d => d.propertyAccessNames)) 214 const allTemplateLiteralImportNames = new Set(fileData.flatMap(d => d.templateLiteralContents.flatMap(extractImportNamesFromTemplateLiteral))) 215 for (const name of allExportNames) { 216 if (allImportedNames.has(name)) continue 217 if (allStringLiterals.has(name) || allPropertyAccess.has(name) || allTemplateLiteralImportNames.has(name)) { 218 allImportedNames.add(name) 222 return { fileData, allImportedNames } 225export const getUnusedExports = async ({ pathsA }: {pathsA: absFileDirPath[]}) => { 226 const byFile = await getUnusedExportsByFile({ pathsA }) 227 return _.uniq(Object.values(byFile).flat()) 230export const getUnusedExportsByFile = async ({ pathsA, extraImportScanPathsA }: {pathsA: absFileDirPath[], extraImportScanPathsA?: absFileDirPath[]}) => { 231 const allScanPaths = extraImportScanPathsA ? [...pathsA, ...extraImportScanPathsA] : pathsA 232 const { fileData, allImportedNames } = await collectFileExportData(allScanPaths) 235 const unusedByFile: Record<string, string[]> = {} 237 for (const { path: filePath, exports, internalRefs } of fileData) { 239 if (!reportPathsSet.has(ptPath)) continue 240 const internalRefsSet = new Set(internalRefs) 241 const unused = exports.filter(name => !allImportedNames.has(name) && !internalRefsSet.has(name)) 242 if (unused.length > 0) unusedByFile[ptPath] = unused