1import type { CreateRule, ESTree, Visitor } from "@oxlint/plugins"
2import * as fs from "node:fs"
3import * as path from "node:path"
4import ts from "typescript"
5
6interface InternalExport {
7 readonly fileName: string
8 readonly declaration: ts.Node
9 readonly name: string
10 readonly range: readonly [number, number]
11 used: boolean
12}
13
14interface Diagnostic {
15 readonly fileName: string
16 readonly range: readonly [number, number]
17 readonly message: string
18}
19
20interface Analysis {
21 readonly diagnosticsByFile: ReadonlyMap<string, ReadonlyArray<Diagnostic>>
22}
23
24interface ImportBinding {
25 readonly fileName: string
26 readonly importedName: string
27}
28
29interface FileInfo {
30 readonly fileName: string
31 readonly sourceFile: ts.SourceFile
32 readonly imports: Map<string, ImportBinding>
33 readonly namespaceImports: Map<string, string>
34 readonly internalExports: Map<string, Array<InternalExport>>
35 readonly publicDeclarations: Array<ts.Node>
36}
37
38const cache = new Map<string, Analysis>()
39const sourceExtensions = new Set([".ts"])
40
41function getCwd(context: { readonly cwd?: string; getCwd?: () => string }): string {
42 return context.cwd ?? context.getCwd?.() ?? process.cwd()
43}
44
45function normalizePathName(filename: string): string {
46 return path.resolve(filename).split(path.sep).join("/")
47}
48
49function rangeNode(range: readonly [number, number]): ESTree.Node {
50 return { range: [range[0], range[1]] } as ESTree.Node
51}
52
53function isSourceFile(fileName: string): boolean {
54 return sourceExtensions.has(path.extname(fileName)) && !fileName.endsWith(".d.ts")
55}
56
57function findSourceFiles(cwd: string): Array<string> {
58 const packagesDir = path.join(cwd, "packages")
59 if (!fs.existsSync(packagesDir)) return []
60
61 const files: Array<string> = []
62 const visit = (directory: string) => {
63 for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
64 const entryPath = path.join(directory, entry.name)
65 if (entry.isDirectory()) {
66 if (entry.name !== "dist" && entry.name !== "build" && entry.name !== "node_modules") {
67 visit(entryPath)
68 }
69 } else if (entry.isFile() && isSourceFile(entryPath) && normalizePathName(entryPath).includes("/src/")) {
70 files.push(entryPath)
71 }
72 }
73 }
74
75 visit(packagesDir)
76 return files
77}
78
79function findWorkspacePackages(cwd: string): Map<string, string> {
80 const packagesDir = path.join(cwd, "packages")
81 const packages = new Map<string, string>()
82 if (!fs.existsSync(packagesDir)) return packages
83
84 const visit = (directory: string) => {
85 const packageJson = path.join(directory, "package.json")
86 if (fs.existsSync(packageJson)) {
87 try {
88 const json = JSON.parse(fs.readFileSync(packageJson, "utf8")) as { readonly name?: unknown }
89 if (typeof json.name === "string") {
90 packages.set(json.name, directory)
91 }
92 } catch {
93 // Ignore malformed package metadata; TypeScript will report that elsewhere.
94 }
95 return
96 }
97
98 for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
99 if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== "dist" && entry.name !== "build") {
100 visit(path.join(directory, entry.name))
101 }
102 }
103 }
104
105 visit(packagesDir)
106 return packages
107}
108
109function resolveSourceFile(basePath: string): string | undefined {
110 const candidates = path.extname(basePath) === ".ts"
111 ? [basePath]
112 : [basePath, `${basePath}.ts`, path.join(basePath, "index.ts")]
113
114 for (const candidate of candidates) {
115 if (fs.existsSync(candidate) && isSourceFile(candidate)) {
116 return normalizePathName(candidate)
117 }
118 }
119 return undefined
120}
121
122function resolveModule(
123 source: string,
124 containingFile: string,
125 workspacePackages: ReadonlyMap<string, string>
126): string | undefined {
127 if (source.startsWith("./") || source.startsWith("../")) {
128 return resolveSourceFile(path.resolve(path.dirname(containingFile), source))
129 }
130
131 let packageName: string | undefined
132 for (const name of workspacePackages.keys()) {
133 if (source === name || source.startsWith(`${name}/`)) {
134 if (packageName === undefined || name.length > packageName.length) {
135 packageName = name
136 }
137 }
138 }
139 if (packageName === undefined) return undefined
140
141 const packageRoot = workspacePackages.get(packageName)
142 if (packageRoot === undefined) return undefined
143
144 if (source === packageName) {
145 return resolveSourceFile(path.join(packageRoot, "src", "index.ts"))
146 }
147
148 const subpath = source.slice(packageName.length + 1)
149 return resolveSourceFile(path.join(packageRoot, "src", subpath))
150}
151
152function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
153 return ts.canHaveModifiers(node) && (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === kind)
154}
155
156function hasExportModifier(node: ts.Node): boolean {
157 return hasModifier(node, ts.SyntaxKind.ExportKeyword)
158}
159
160function hasPrivateOrProtectedModifier(node: ts.Node): boolean {
161 return hasModifier(node, ts.SyntaxKind.PrivateKeyword) || hasModifier(node, ts.SyntaxKind.ProtectedKeyword)
162}
163
164function hasInternalJSDoc(node: ts.Node): boolean {
165 return ts.getJSDocTags(node).some((tag) => tag.tagName.getText() === "internal")
166}
167
168function hasInternalApiJSDoc(node: ts.Node): boolean {
169 if (hasInternalJSDoc(node)) return true
170 if (ts.isVariableDeclaration(node)) {
171 return hasInternalJSDoc(node.parent.parent)
172 }
173 if (ts.isBindingElement(node) && ts.isVariableDeclaration(node.parent.parent)) {
174 return hasInternalJSDoc(node.parent.parent.parent)
175 }
176 return false
177}
178
179function isInternalPath(fileName: string): boolean {
180 return /\/src\/(?:.*\/)?internal\//.test(normalizePathName(fileName))
181}
182
183function collectBindingNames(name: ts.BindingName, out: Array<ts.Identifier>) {
184 if (ts.isIdentifier(name)) {
185 out.push(name)
186 return
187 }
188 for (const element of name.elements) {
189 if (ts.isBindingElement(element)) {
190 collectBindingNames(element.name, out)
191 }
192 }
193}
194
195function getTopLevelDeclarationNameNodes(node: ts.Node): Array<ts.Identifier> {
196 if (
197 (ts.isFunctionDeclaration(node) ||
198 ts.isClassDeclaration(node) ||
199 ts.isInterfaceDeclaration(node) ||
200 ts.isTypeAliasDeclaration(node) ||
201 ts.isEnumDeclaration(node) ||
202 ts.isModuleDeclaration(node)) &&
203 node.name !== undefined &&
204 ts.isIdentifier(node.name)
205 ) {
206 return [node.name]
207 }
208
209 if (ts.isVariableStatement(node)) {
210 const names: Array<ts.Identifier> = []
211 for (const declaration of node.declarationList.declarations) {
212 collectBindingNames(declaration.name, names)
213 }
214 return names
215 }
216
217 return []
218}
219
220function getDeclarationForName(node: ts.Node, name: ts.Identifier): ts.Node {
221 if (!ts.isVariableStatement(node)) return node
222
223 let result: ts.Node = node
224 const visit = (current: ts.Node) => {
225 if (ts.isVariableDeclaration(current) || ts.isBindingElement(current)) {
226 const names: Array<ts.Identifier> = []
227 collectBindingNames(current.name, names)
228 if (names.includes(name)) {
229 result = current
230 return
231 }
232 }
233 ts.forEachChild(current, visit)
234 }
235 visit(node.declarationList)
236 return result
237}
238
239function isImportOrExportBinding(identifier: ts.Identifier): boolean {
240 const parent = identifier.parent
241 return (ts.isImportSpecifier(parent) && (parent.name === identifier || parent.propertyName === identifier)) ||
242 (ts.isImportClause(parent) && parent.name === identifier) ||
243 (ts.isNamespaceImport(parent) && parent.name === identifier) ||
244 (ts.isExportSpecifier(parent) && (parent.name === identifier || parent.propertyName === identifier))
245}
246
247function isDeclarationName(identifier: ts.Identifier): boolean {
248 const parent = identifier.parent
249 return (ts.isVariableDeclaration(parent) && parent.name === identifier) ||
250 (ts.isBindingElement(parent) && parent.name === identifier) ||
251 (ts.isParameter(parent) && parent.name === identifier) ||
252 (ts.isFunctionDeclaration(parent) && parent.name === identifier) ||
253 (ts.isClassDeclaration(parent) && parent.name === identifier) ||
254 (ts.isInterfaceDeclaration(parent) && parent.name === identifier) ||
255 (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) ||
256 (ts.isEnumDeclaration(parent) && parent.name === identifier) ||
257 (ts.isEnumMember(parent) && parent.name === identifier) ||
258 (ts.isModuleDeclaration(parent) && parent.name === identifier)
259}
260
261function isNonReferenceName(identifier: ts.Identifier): boolean {
262 const parent = identifier.parent
263 return isImportOrExportBinding(identifier) ||
264 isDeclarationName(identifier) ||
265 (ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
266 (ts.isPropertyAssignment(parent) && parent.name === identifier) ||
267 (ts.isPropertyDeclaration(parent) && parent.name === identifier) ||
268 (ts.isPropertySignature(parent) && parent.name === identifier) ||
269 (ts.isMethodDeclaration(parent) && parent.name === identifier) ||
270 (ts.isMethodSignature(parent) && parent.name === identifier) ||
271 (ts.isGetAccessorDeclaration(parent) && parent.name === identifier) ||
272 (ts.isSetAccessorDeclaration(parent) && parent.name === identifier)
273}
274
275function isInside(node: ts.Node, container: ts.Node): boolean {
276 if (node.getSourceFile() !== container.getSourceFile()) return false
277 return node.getStart() >= container.getStart() && node.getEnd() <= container.getEnd()
278}
279
280function getExports(
281 fileInfos: ReadonlyMap<string, FileInfo>,
282 fileName: string | undefined,
283 exportName: string
284): ReadonlyArray<InternalExport> {
285 if (fileName === undefined) return []
286 return fileInfos.get(fileName)?.internalExports.get(exportName) ?? []
287}
288
289function getReExportedInternals(
290 fileInfos: ReadonlyMap<string, FileInfo>,
291 fileInfo: FileInfo,
292 workspacePackages: ReadonlyMap<string, string>,
293 specifier: ts.ExportSpecifier
294): ReadonlyArray<InternalExport> {
295 const exportDeclaration = specifier.parent.parent
296 const importedName = (specifier.propertyName ?? specifier.name).text
297 if (
298 exportDeclaration.moduleSpecifier !== undefined &&
299 ts.isStringLiteral(exportDeclaration.moduleSpecifier)
300 ) {
301 const importedFile = resolveModule(
302 exportDeclaration.moduleSpecifier.text,
303 fileInfo.sourceFile.fileName,
304 workspacePackages
305 )
306 return getExports(fileInfos, importedFile, importedName)
307 }
308
309 const imported = fileInfo.imports.get(importedName)
310 return [
311 ...(fileInfo.internalExports.get(importedName) ?? []),
312 ...getExports(fileInfos, imported?.fileName, imported?.importedName ?? "")
313 ]
314}
315
316function markUsed(exports: ReadonlyArray<InternalExport>, node: ts.Node) {
317 for (const internal of exports) {
318 if (!isInside(node, internal.declaration)) {
319 internal.used = true
320 }
321 }
322}
323
324function addFileDiagnostic(
325 diagnosticsByFile: Map<string, Array<Diagnostic>>,
326 diagnostic: Diagnostic
327) {
328 const normalized = normalizePathName(diagnostic.fileName)
329 const diagnostics = diagnosticsByFile.get(normalized) ?? []
330 diagnostics.push(diagnostic)
331 diagnosticsByFile.set(normalized, diagnostics)
332}
333
334function addInternalExport(fileInfo: FileInfo, internal: InternalExport) {
335 const exports = fileInfo.internalExports.get(internal.name) ?? []
336 exports.push(internal)
337 fileInfo.internalExports.set(internal.name, exports)
338}
339
340function collectFileInfo(
341 sourceFile: ts.SourceFile,
342 workspacePackages: ReadonlyMap<string, string>
343): FileInfo {
344 const fileInfo: FileInfo = {
345 fileName: normalizePathName(sourceFile.fileName),
346 sourceFile,
347 imports: new Map(),
348 namespaceImports: new Map(),
349 internalExports: new Map(),
350 publicDeclarations: []
351 }
352 const publicDeclarationSet = new Set<ts.Node>()
353 const isInternalFile = isInternalPath(sourceFile.fileName)
354
355 for (const statement of sourceFile.statements) {
356 if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
357 const importedFile = resolveModule(statement.moduleSpecifier.text, sourceFile.fileName, workspacePackages)
358 const importClause = statement.importClause
359 if (importedFile === undefined || importClause === undefined) continue
360
361 if (importClause.name !== undefined) {
362 fileInfo.imports.set(importClause.name.text, { fileName: importedFile, importedName: "default" })
363 }
364
365 const namedBindings = importClause.namedBindings
366 if (namedBindings === undefined) continue
367 if (ts.isNamespaceImport(namedBindings)) {
368 fileInfo.namespaceImports.set(namedBindings.name.text, importedFile)
369 } else {
370 for (const specifier of namedBindings.elements) {
371 const importedName = (specifier.propertyName ?? specifier.name).text
372 fileInfo.imports.set(specifier.name.text, { fileName: importedFile, importedName })
373 }
374 }
375 continue
376 }
377
378 if (
379 !isInternalFile &&
380 ts.isExportDeclaration(statement) &&
381 !hasInternalApiJSDoc(statement) &&
382 statement.exportClause !== undefined &&
383 ts.isNamedExports(statement.exportClause)
384 ) {
385 fileInfo.publicDeclarations.push(statement)
386 continue
387 }
388
389 if (!hasExportModifier(statement)) continue
390
391 const names = getTopLevelDeclarationNameNodes(statement)
392 if (names.length === 0) continue
393
394 const statementIsInternal = hasInternalApiJSDoc(statement)
395 for (const name of names) {
396 const declaration = getDeclarationForName(statement, name)
397 const declarationIsInternal = statementIsInternal || hasInternalApiJSDoc(declaration)
398 if (declarationIsInternal) {
399 addInternalExport(fileInfo, {
400 fileName: sourceFile.fileName,
401 declaration,
402 name: name.text,
403 range: [name.getStart(sourceFile), name.getEnd()],
404 used: false
405 })
406 } else if (!isInternalFile && !publicDeclarationSet.has(statement)) {
407 fileInfo.publicDeclarations.push(statement)
408 publicDeclarationSet.add(statement)
409 }
410 }
411 }
412
413 return fileInfo
414}
415
416function markNamespaceReference(
417 fileInfos: ReadonlyMap<string, FileInfo>,
418 fileInfo: FileInfo,
419 namespace: ts.Identifier,
420 name: ts.Identifier
421) {
422 const importedFile = fileInfo.namespaceImports.get(namespace.text)
423 markUsed(getExports(fileInfos, importedFile, name.text), name)
424}
425
426function scanUsage(
427 fileInfos: ReadonlyMap<string, FileInfo>,
428 fileInfo: FileInfo,
429 workspacePackages: ReadonlyMap<string, string>
430) {
431 const visit = (node: ts.Node) => {
432 if (ts.isExportSpecifier(node)) {
433 markUsed(getReExportedInternals(fileInfos, fileInfo, workspacePackages, node), node)
434 } else if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && ts.isIdentifier(node.name)) {
435 markNamespaceReference(fileInfos, fileInfo, node.expression, node.name)
436 } else if (ts.isQualifiedName(node) && ts.isIdentifier(node.left)) {
437 markNamespaceReference(fileInfos, fileInfo, node.left, node.right)
438 } else if (ts.isIdentifier(node) && !isNonReferenceName(node)) {
439 markUsed(fileInfo.internalExports.get(node.text) ?? [], node)
440 const imported = fileInfo.imports.get(node.text)
441 markUsed(getExports(fileInfos, imported?.fileName, imported?.importedName ?? ""), node)
442 }
443 ts.forEachChild(node, visit)
444 }
445 visit(fileInfo.sourceFile)
446}
447
448function findInternalReferences(
449 fileInfos: ReadonlyMap<string, FileInfo>,
450 fileInfo: FileInfo,
451 diagnostics: Array<Diagnostic>,
452 node: ts.Node
453) {
454 const visit = (current: ts.Node) => {
455 if (ts.isPropertyAccessExpression(current) && ts.isIdentifier(current.expression)) {
456 const importedFile = fileInfo.namespaceImports.get(current.expression.text)
457 for (const internal of getExports(fileInfos, importedFile, current.name.text)) {
458 diagnostics.push({
459 fileName: fileInfo.fileName,
460 range: [current.name.getStart(), current.name.getEnd()],
461 message: `Do not reference @internal export "${internal.name}" in a public exported type signature`
462 })
463 }
464 } else if (ts.isQualifiedName(current) && ts.isIdentifier(current.left)) {
465 const importedFile = fileInfo.namespaceImports.get(current.left.text)
466 for (const internal of getExports(fileInfos, importedFile, current.right.text)) {
467 diagnostics.push({
468 fileName: fileInfo.fileName,
469 range: [current.right.getStart(), current.right.getEnd()],
470 message: `Do not reference @internal export "${internal.name}" in a public exported type signature`
471 })
472 }
473 } else if (ts.isIdentifier(current) && !isNonReferenceName(current)) {
474 const localExports = fileInfo.internalExports.get(current.text) ?? []
475 const imported = fileInfo.imports.get(current.text)
476 const importedExports = getExports(fileInfos, imported?.fileName, imported?.importedName ?? "")
477
478 for (const internal of [...localExports, ...importedExports]) {
479 diagnostics.push({
480 fileName: fileInfo.fileName,
481 range: [current.getStart(), current.getEnd()],
482 message: `Do not reference @internal export "${internal.name}" in a public exported type signature`
483 })
484 }
485 }
486 ts.forEachChild(current, visit)
487 }
488 visit(node)
489}
490
491function scanTypeParameters(
492 fileInfos: ReadonlyMap<string, FileInfo>,
493 fileInfo: FileInfo,
494 diagnostics: Array<Diagnostic>,
495 typeParameters: ts.NodeArray<ts.TypeParameterDeclaration> | undefined
496) {
497 if (typeParameters === undefined) return
498 for (const typeParameter of typeParameters) {
499 if (typeParameter.constraint !== undefined) {
500 findInternalReferences(fileInfos, fileInfo, diagnostics, typeParameter.constraint)
501 }
502 if (typeParameter.default !== undefined) {
503 findInternalReferences(fileInfos, fileInfo, diagnostics, typeParameter.default)
504 }
505 }
506}
507
508function scanParameterTypes(
509 fileInfos: ReadonlyMap<string, FileInfo>,
510 fileInfo: FileInfo,
511 diagnostics: Array<Diagnostic>,
512 parameters: ts.NodeArray<ts.ParameterDeclaration>
513) {
514 for (const parameter of parameters) {
515 if (parameter.type !== undefined) {
516 findInternalReferences(fileInfos, fileInfo, diagnostics, parameter.type)
517 }
518 }
519}
520
521function scanFunctionLikeSignature(
522 fileInfos: ReadonlyMap<string, FileInfo>,
523 fileInfo: FileInfo,
524 diagnostics: Array<Diagnostic>,
525 node: ts.SignatureDeclarationBase
526) {
527 scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters)
528 scanParameterTypes(fileInfos, fileInfo, diagnostics, node.parameters)
529 if (node.type !== undefined) {
530 findInternalReferences(fileInfos, fileInfo, diagnostics, node.type)
531 }
532}
533
534function scanClassMemberSignature(
535 fileInfos: ReadonlyMap<string, FileInfo>,
536 fileInfo: FileInfo,
537 diagnostics: Array<Diagnostic>,
538 member: ts.ClassElement
539) {
540 if (hasInternalJSDoc(member)) return
541 if (hasPrivateOrProtectedModifier(member)) return
542 if (
543 ts.isMethodDeclaration(member) ||
544 ts.isConstructorDeclaration(member) ||
545 ts.isGetAccessorDeclaration(member) ||
546 ts.isSetAccessorDeclaration(member)
547 ) {
548 scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, member)
549 } else if (ts.isPropertyDeclaration(member) && member.type !== undefined) {
550 findInternalReferences(fileInfos, fileInfo, diagnostics, member.type)
551 }
552}
553
554function scanInterfaceMemberSignature(
555 fileInfos: ReadonlyMap<string, FileInfo>,
556 fileInfo: FileInfo,
557 diagnostics: Array<Diagnostic>,
558 member: ts.TypeElement
559) {
560 if (hasInternalJSDoc(member)) return
561 if (
562 ts.isMethodSignature(member) || ts.isCallSignatureDeclaration(member) || ts.isConstructSignatureDeclaration(member)
563 ) {
564 scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, member)
565 } else if (ts.isPropertySignature(member) && member.type !== undefined) {
566 findInternalReferences(fileInfos, fileInfo, diagnostics, member.type)
567 } else if (ts.isIndexSignatureDeclaration(member)) {
568 scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, member)
569 }
570}
571
572function scanPublicSignature(
573 fileInfos: ReadonlyMap<string, FileInfo>,
574 fileInfo: FileInfo,
575 diagnostics: Array<Diagnostic>,
576 node: ts.Node,
577 workspacePackages: ReadonlyMap<string, string>
578) {
579 if (ts.isFunctionDeclaration(node)) {
580 scanFunctionLikeSignature(fileInfos, fileInfo, diagnostics, node)
581 } else if (ts.isVariableStatement(node)) {
582 for (const declaration of node.declarationList.declarations) {
583 if (declaration.type !== undefined) {
584 findInternalReferences(fileInfos, fileInfo, diagnostics, declaration.type)
585 }
586 }
587 } else if (ts.isTypeAliasDeclaration(node)) {
588 scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters)
589 findInternalReferences(fileInfos, fileInfo, diagnostics, node.type)
590 } else if (ts.isInterfaceDeclaration(node)) {
591 scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters)
592 for (const heritage of node.heritageClauses ?? []) {
593 for (const type of heritage.types) {
594 findInternalReferences(fileInfos, fileInfo, diagnostics, type)
595 }
596 }
597 for (const member of node.members) {
598 scanInterfaceMemberSignature(fileInfos, fileInfo, diagnostics, member)
599 }
600 } else if (ts.isClassDeclaration(node)) {
601 scanTypeParameters(fileInfos, fileInfo, diagnostics, node.typeParameters)
602 for (const heritage of node.heritageClauses ?? []) {
603 for (const type of heritage.types) {
604 findInternalReferences(fileInfos, fileInfo, diagnostics, type)
605 }
606 }
607 for (const member of node.members) {
608 scanClassMemberSignature(fileInfos, fileInfo, diagnostics, member)
609 }
610 } else if (
611 ts.isExportDeclaration(node) &&
612 node.exportClause !== undefined &&
613 ts.isNamedExports(node.exportClause)
614 ) {
615 for (const specifier of node.exportClause.elements) {
616 for (const internal of getReExportedInternals(fileInfos, fileInfo, workspacePackages, specifier)) {
617 const name = specifier.propertyName ?? specifier.name
618 diagnostics.push({
619 fileName: fileInfo.fileName,
620 range: [name.getStart(), name.getEnd()],
621 message: `Do not re-export @internal export "${internal.name}" from a public module`
622 })
623 }
624 }
625 }
626}
627
628function analyze(cwd: string): Analysis {
629 const normalizedCwd = normalizePathName(cwd)
630 const cached = cache.get(normalizedCwd)
631 if (cached !== undefined) return cached
632
633 const workspacePackages = findWorkspacePackages(cwd)
634 const fileInfos = new Map<string, FileInfo>()
635 for (const fileName of findSourceFiles(cwd)) {
636 const sourceFile = ts.createSourceFile(fileName, fs.readFileSync(fileName, "utf8"), ts.ScriptTarget.Latest, true)
637 const fileInfo = collectFileInfo(sourceFile, workspacePackages)
638 fileInfos.set(fileInfo.fileName, fileInfo)
639 }
640
641 for (const fileInfo of fileInfos.values()) {
642 scanUsage(fileInfos, fileInfo, workspacePackages)
643 }
644
645 const diagnosticsByFile = new Map<string, Array<Diagnostic>>()
646
647 for (const fileInfo of fileInfos.values()) {
648 const diagnostics: Array<Diagnostic> = []
649 for (const declaration of fileInfo.publicDeclarations) {
650 scanPublicSignature(fileInfos, fileInfo, diagnostics, declaration, workspacePackages)
651 }
652 for (const diagnostic of diagnostics) {
653 addFileDiagnostic(diagnosticsByFile, diagnostic)
654 }
655
656 for (const exportsForName of fileInfo.internalExports.values()) {
657 for (const internal of exportsForName) {
658 if (!internal.used) {
659 addFileDiagnostic(diagnosticsByFile, {
660 fileName: internal.fileName,
661 range: internal.range,
662 message: `@internal export "${internal.name}" is not used by production sources`
663 })
664 }
665 }
666 }
667 }
668
669 for (const diagnostics of diagnosticsByFile.values()) {
670 diagnostics.sort((self, that) => self.range[0] - that.range[0])
671 }
672
673 const analysis = { diagnosticsByFile }
674 cache.set(normalizedCwd, analysis)
675 return analysis
676}
677
678const rule: CreateRule = {
679 meta: {
680 type: "problem",
681 docs: {
682 description: "Disallow unused @internal exports, public re-exports, and references from public type signatures"
683 }
684 },
685 create(context) {
686 const analysis = analyze(getCwd(context))
687 const diagnostics = analysis.diagnosticsByFile.get(normalizePathName(context.filename)) ?? []
688
689 return {
690 Program() {
691 for (const diagnostic of diagnostics) {
692 context.report({
693 node: rangeNode(diagnostic.range),
694 message: diagnostic.message
695 })
696 }
697 }
698 } as Visitor
699 }
700}
701
702export default rule