🌳
pt0/deployF/acornF/acornSingleFileF.mts
2import * as _ from 'lodash-es'
3import { acornParse } from './acornParseF.mts'
4import ts from 'typescript'
5import { getIsTsExt, resetAcornParseCount, type AcornNode, type AcornProgram } from "./libAcornF.mts"
8import { tsAbsPath, type absFileDirPath } from '../../ptDirF.mts'
13import fs from 'fs'
15type TsNode = ts.Node & {[k: string]: any}
17export const getModuleDefNameH = (contents: string, { ptPath }: {ptPath: string}) => {
18 const tsExt = getIsTsExt(tsAbsPath(ptPath))
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[] = []
28 if (tsExt) {
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)
39 }
40 })
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)
46 }
47 if (ts.isParameter(node) && ts.isIdentifier(node.name)) {
48 allLocalDeclarations.add(node.name.text)
49 }
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)
62 }
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(''))
67 }
69 ts.forEachChild(node, (child) => visit(child, node))
70 }
72 visit(sourceFile, null)
74 } else {
75 const ast = acornParse({ contents, ptPath: tsAbsPath(ptPath) })
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)
86 }
87 }
88 } else if (node.specifiers) {
89 for (const spec of node.specifiers as AcornNode[]) exportA.push(spec.local.name)
90 }
91 } else if (node.type === 'VariableDeclaration') {
92 for (const decl of node.declarations) {
93 if (decl.id.type === 'Identifier') privateA.push(decl.id.name)
94 }
95 } else if (node.type === 'FunctionDeclaration' && node.id) {
96 privateA.push(node.id.name)
97 }
98 }
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)
106 }
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)
111 }
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)
115 }
116 }
118 if (node.type === 'JSXIdentifier' && node.name && /^[A-Z]/.test(node.name) && parent?.type === 'JSXOpeningElement') {
119 referencedA.add(node.name)
120 }
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)
129 );
130 if (!isDecl) referencedA.add(node.name)
131 if (parent?.type === 'MemberExpression' && parent.property === node && !parent.computed) propertyAccessNamesA.push(node.name)
132 }
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(''))
136 }
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);
143 }
144 }
145 }
146 findReferences(ast, null);
147 }
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) => {
156 const absPath = pathDownJoin(ptDir, ptPath)
157 const mtime = fs.statSync(absPath, {throwIfNoEntry: false})?.mtimeMs || 0
158 return runMemoTempfile({cacheKeyA: ['fileExportData-v1', ptPath, String(mtime)]}, async () => {
159 const contents = await read1File(absPath)
160 if (contents.includes('//pt' + 'noshake')) return null
161 return getModuleDefNameH(contents, { ptPath })
162 })
165const extractImportNamesFromTemplateLiteral = (templateContent: string): string[] => {
166 try {
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)
174 }
175 }
176 return names
177 } catch { return [] }
180const collectFileExportData = async (pathsA: absFileDirPath[]) => {
181 let visited = 0
182 const fileData = await allPromCalls(pathsA, async (filePath) => {
183 if (++visited % 500 === 0) resetAcornParseCount() // sanctioned repo-wide bulk walk — exempt from OOM breaker (same pattern as ptMadge)
184 const ptPath = absPathToPtPath(filePath)
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
190 const imports = await acornSingleFile(ptPath as unknown as absFileDirPath)
191 return { path: filePath, exports: modData.exportA, internalRefs: modData.referencedA, imports,
192 stringLiteralValues: modData.stringLiteralValuesA, templateLiteralContents: modData.templateLiteralContentsA, propertyAccessNames: modData.propertyAccessNamesA }
193 })
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)
201 }
202 }
203 for (const fd of fileData) {
204 if (starImportedFiles.has(absPathToPtPath(fd.path))) {
205 for (const name of fd.exports) allImportedNames.add(name)
206 }
207 }
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)
219 }
220 }
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)
234 const reportPathsSet = new Set(pathsA.map(p => absPathToPtPath(p)))
235 const unusedByFile: Record<string, string[]> = {}
237 for (const { path: filePath, exports, internalRefs } of fileData) {
238 const ptPath = absPathToPtPath(filePath)
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
243 }
245 return unusedByFile