Skip to content

Commit db7ed2a

Browse files
committed
chore: wip
1 parent af08d8d commit db7ed2a

File tree

3 files changed

+368
-13
lines changed

3 files changed

+368
-13
lines changed

src/extractor.ts

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export function extractDeclarations(sourceCode: string, filePath: string): Decla
8080
}
8181

8282
visitTopLevel(sourceFile)
83+
84+
// Second pass: Find referenced types that aren't imported or declared
85+
const referencedTypes = findReferencedTypes(declarations, sourceCode)
86+
const additionalDeclarations = extractReferencedTypeDeclarations(sourceFile, referencedTypes, sourceCode)
87+
declarations.push(...additionalDeclarations)
88+
8389
return declarations
8490
}
8591

@@ -1050,3 +1056,183 @@ function shouldIncludeNonExportedInterface(interfaceName: string, sourceCode: st
10501056

10511057
return exportedFunctionPattern.test(sourceCode) || exportedTypePattern.test(sourceCode)
10521058
}
1059+
1060+
/**
1061+
* Find types that are referenced in declarations but not imported or declared
1062+
*/
1063+
function findReferencedTypes(declarations: Declaration[], sourceCode: string): Set<string> {
1064+
const referencedTypes = new Set<string>()
1065+
const importedTypes = new Set<string>()
1066+
const declaredTypes = new Set<string>()
1067+
1068+
// Collect imported types
1069+
for (const decl of declarations) {
1070+
if (decl.kind === 'import') {
1071+
// Extract imported type names from import statements
1072+
const importMatches = decl.text.match(/import\s+(?:type\s+)?\{([^}]+)\}/g)
1073+
if (importMatches) {
1074+
for (const match of importMatches) {
1075+
const items = match.replace(/import\s+(?:type\s+)?\{([^}]+)\}/, '$1').split(',')
1076+
for (const item of items) {
1077+
const cleanItem = item.replace(/^type\s+/, '').trim()
1078+
importedTypes.add(cleanItem)
1079+
}
1080+
}
1081+
}
1082+
}
1083+
}
1084+
1085+
// Collect declared types (including those within modules/namespaces)
1086+
for (const decl of declarations) {
1087+
if (['interface', 'type', 'class', 'enum'].includes(decl.kind)) {
1088+
declaredTypes.add(decl.name)
1089+
}
1090+
// Also scan module/namespace bodies for declared types
1091+
if (decl.kind === 'module') {
1092+
const moduleTypes = extractTypesFromModuleText(decl.text)
1093+
moduleTypes.forEach((type: string) => declaredTypes.add(type))
1094+
}
1095+
}
1096+
1097+
// Find referenced types in declaration texts
1098+
for (const decl of declarations) {
1099+
if (decl.kind !== 'import' && decl.kind !== 'export') {
1100+
// Look for type references in the declaration text
1101+
const typeReferences = decl.text.match(/:\s*([A-Z][a-zA-Z0-9]*)/g) || []
1102+
for (const ref of typeReferences) {
1103+
const typeName = ref.replace(/:\s*/, '')
1104+
// Only add if it's not imported, not declared, and not a built-in type
1105+
if (!importedTypes.has(typeName) && !declaredTypes.has(typeName) && !isBuiltInType(typeName)) {
1106+
referencedTypes.add(typeName)
1107+
}
1108+
}
1109+
}
1110+
}
1111+
1112+
return referencedTypes
1113+
}
1114+
1115+
/**
1116+
* Extract declarations for referenced types by searching the entire source file
1117+
*/
1118+
function extractReferencedTypeDeclarations(sourceFile: ts.SourceFile, referencedTypes: Set<string>, sourceCode: string): Declaration[] {
1119+
const additionalDeclarations: Declaration[] = []
1120+
1121+
if (referencedTypes.size === 0) {
1122+
return additionalDeclarations
1123+
}
1124+
1125+
// Visit all nodes in the source file to find interface/type/class/enum declarations
1126+
function visitAllNodes(node: ts.Node) {
1127+
switch (node.kind) {
1128+
case ts.SyntaxKind.InterfaceDeclaration:
1129+
const interfaceNode = node as ts.InterfaceDeclaration
1130+
const interfaceName = interfaceNode.name.getText()
1131+
if (referencedTypes.has(interfaceName)) {
1132+
const decl = extractInterfaceDeclaration(interfaceNode, sourceCode)
1133+
additionalDeclarations.push(decl)
1134+
referencedTypes.delete(interfaceName) // Remove to avoid duplicates
1135+
}
1136+
break
1137+
1138+
case ts.SyntaxKind.TypeAliasDeclaration:
1139+
const typeNode = node as ts.TypeAliasDeclaration
1140+
const typeName = typeNode.name.getText()
1141+
if (referencedTypes.has(typeName)) {
1142+
const decl = extractTypeAliasDeclaration(typeNode, sourceCode)
1143+
additionalDeclarations.push(decl)
1144+
referencedTypes.delete(typeName)
1145+
}
1146+
break
1147+
1148+
case ts.SyntaxKind.ClassDeclaration:
1149+
const classNode = node as ts.ClassDeclaration
1150+
if (classNode.name) {
1151+
const className = classNode.name.getText()
1152+
if (referencedTypes.has(className)) {
1153+
const decl = extractClassDeclaration(classNode, sourceCode)
1154+
additionalDeclarations.push(decl)
1155+
referencedTypes.delete(className)
1156+
}
1157+
}
1158+
break
1159+
1160+
case ts.SyntaxKind.EnumDeclaration:
1161+
const enumNode = node as ts.EnumDeclaration
1162+
const enumName = enumNode.name.getText()
1163+
if (referencedTypes.has(enumName)) {
1164+
const decl = extractEnumDeclaration(enumNode, sourceCode)
1165+
additionalDeclarations.push(decl)
1166+
referencedTypes.delete(enumName)
1167+
}
1168+
break
1169+
}
1170+
1171+
// Continue visiting child nodes
1172+
ts.forEachChild(node, visitAllNodes)
1173+
}
1174+
1175+
visitAllNodes(sourceFile)
1176+
return additionalDeclarations
1177+
}
1178+
1179+
/**
1180+
* Extract type names from module/namespace text
1181+
*/
1182+
function extractTypesFromModuleText(moduleText: string): string[] {
1183+
const types: string[] = []
1184+
1185+
// Look for interface declarations
1186+
const interfaceMatches = moduleText.match(/(?:export\s+)?interface\s+([A-Z][a-zA-Z0-9]*)/g)
1187+
if (interfaceMatches) {
1188+
interfaceMatches.forEach(match => {
1189+
const name = match.replace(/(?:export\s+)?interface\s+/, '')
1190+
types.push(name)
1191+
})
1192+
}
1193+
1194+
// Look for type alias declarations
1195+
const typeMatches = moduleText.match(/(?:export\s+)?type\s+([A-Z][a-zA-Z0-9]*)/g)
1196+
if (typeMatches) {
1197+
typeMatches.forEach(match => {
1198+
const name = match.replace(/(?:export\s+)?type\s+/, '')
1199+
types.push(name)
1200+
})
1201+
}
1202+
1203+
// Look for class declarations
1204+
const classMatches = moduleText.match(/(?:export\s+)?(?:declare\s+)?class\s+([A-Z][a-zA-Z0-9]*)/g)
1205+
if (classMatches) {
1206+
classMatches.forEach(match => {
1207+
const name = match.replace(/(?:export\s+)?(?:declare\s+)?class\s+/, '')
1208+
types.push(name)
1209+
})
1210+
}
1211+
1212+
// Look for enum declarations
1213+
const enumMatches = moduleText.match(/(?:export\s+)?(?:declare\s+)?(?:const\s+)?enum\s+([A-Z][a-zA-Z0-9]*)/g)
1214+
if (enumMatches) {
1215+
enumMatches.forEach(match => {
1216+
const name = match.replace(/(?:export\s+)?(?:declare\s+)?(?:const\s+)?enum\s+/, '')
1217+
types.push(name)
1218+
})
1219+
}
1220+
1221+
return types
1222+
}
1223+
1224+
/**
1225+
* Check if a type is a built-in TypeScript type
1226+
*/
1227+
function isBuiltInType(typeName: string): boolean {
1228+
const builtInTypes = new Set([
1229+
'string', 'number', 'boolean', 'object', 'any', 'unknown', 'never', 'void',
1230+
'undefined', 'null', 'Array', 'Promise', 'Record', 'Partial', 'Required',
1231+
'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable', 'ReturnType',
1232+
'Parameters', 'ConstructorParameters', 'InstanceType', 'ThisType',
1233+
'Function', 'Date', 'RegExp', 'Error', 'Map', 'Set', 'WeakMap', 'WeakSet',
1234+
// Common generic type parameters
1235+
'T', 'K', 'V', 'U', 'R', 'P', 'E', 'A', 'B', 'C', 'D', 'F', 'G', 'H', 'I', 'J', 'L', 'M', 'N', 'O', 'Q', 'S', 'W', 'X', 'Y', 'Z'
1236+
])
1237+
return builtInTypes.has(typeName)
1238+
}

src/processor.ts

Lines changed: 155 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,96 @@
11
/* eslint-disable regexp/no-super-linear-backtracking, regexp/no-misleading-capturing-group, regexp/optimal-quantifier-concatenation */
22
import type { Declaration, ProcessingContext } from './types'
33

4+
/**
5+
* Replace unresolved types with 'any' in the DTS output
6+
*/
7+
function replaceUnresolvedTypes(dtsContent: string, declarations: Declaration[], imports: Declaration[]): string {
8+
// Get all imported type names
9+
const importedTypes = new Set<string>()
10+
for (const imp of imports) {
11+
const allImportedItems = extractAllImportedItems(imp.text)
12+
allImportedItems.forEach(item => importedTypes.add(item))
13+
}
14+
15+
// Get all declared type names (interfaces, types, classes, enums)
16+
const declaredTypes = new Set<string>()
17+
for (const decl of declarations) {
18+
if (['interface', 'type', 'class', 'enum'].includes(decl.kind)) {
19+
declaredTypes.add(decl.name)
20+
}
21+
}
22+
23+
// Common TypeScript built-in types that don't need to be imported
24+
const builtInTypes = new Set([
25+
'string', 'number', 'boolean', 'object', 'any', 'unknown', 'never', 'void',
26+
'undefined', 'null', 'Array', 'Promise', 'Record', 'Partial', 'Required',
27+
'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable', 'ReturnType',
28+
'Parameters', 'ConstructorParameters', 'InstanceType', 'ThisType',
29+
'Function', 'Date', 'RegExp', 'Error', 'Map', 'Set', 'WeakMap', 'WeakSet'
30+
])
31+
32+
// Common generic type parameter names that should not be replaced
33+
const genericTypeParams = new Set([
34+
'T', 'K', 'V', 'U', 'R', 'P', 'E', 'A', 'B', 'C', 'D', 'F', 'G', 'H', 'I', 'J', 'L', 'M', 'N', 'O', 'Q', 'S', 'W', 'X', 'Y', 'Z'
35+
])
36+
37+
// Extract all types that are actually defined in the DTS content itself
38+
// This catches types that weren't extracted but are still defined in the output
39+
const definedInDts = new Set<string>()
40+
41+
// Look for interface definitions
42+
const interfaceMatches = dtsContent.match(/(?:export\s+)?(?:declare\s+)?interface\s+([A-Z][a-zA-Z0-9]*)/g)
43+
if (interfaceMatches) {
44+
interfaceMatches.forEach(match => {
45+
const name = match.replace(/(?:export\s+)?(?:declare\s+)?interface\s+/, '')
46+
definedInDts.add(name)
47+
})
48+
}
49+
50+
// Look for type alias definitions
51+
const typeMatches = dtsContent.match(/(?:export\s+)?(?:declare\s+)?type\s+([A-Z][a-zA-Z0-9]*)/g)
52+
if (typeMatches) {
53+
typeMatches.forEach(match => {
54+
const name = match.replace(/(?:export\s+)?(?:declare\s+)?type\s+/, '')
55+
definedInDts.add(name)
56+
})
57+
}
58+
59+
// Look for class definitions
60+
const classMatches = dtsContent.match(/(?:export\s+)?(?:declare\s+)?class\s+([A-Z][a-zA-Z0-9]*)/g)
61+
if (classMatches) {
62+
classMatches.forEach(match => {
63+
const name = match.replace(/(?:export\s+)?(?:declare\s+)?class\s+/, '')
64+
definedInDts.add(name)
65+
})
66+
}
67+
68+
// Look for enum definitions
69+
const enumMatches = dtsContent.match(/(?:export\s+)?(?:declare\s+)?(?:const\s+)?enum\s+([A-Z][a-zA-Z0-9]*)/g)
70+
if (enumMatches) {
71+
enumMatches.forEach(match => {
72+
const name = match.replace(/(?:export\s+)?(?:declare\s+)?(?:const\s+)?enum\s+/, '')
73+
definedInDts.add(name)
74+
})
75+
}
76+
77+
// Only replace types that are:
78+
// 1. Not imported
79+
// 2. Not declared in our extracted declarations
80+
// 3. Not built-in TypeScript types
81+
// 4. Not generic type parameters
82+
// 5. Not defined anywhere in the DTS content itself
83+
// 6. Actually used as types (not values)
84+
// 7. Have specific patterns that indicate they're problematic
85+
86+
let result = dtsContent
87+
88+
// For now, don't do any automatic type replacement
89+
// The proper solution is to improve the extractor to find all referenced types
90+
91+
return result
92+
}
93+
494
/**
595
* Extract all imported items from an import statement
696
*/
@@ -149,14 +239,23 @@ export function processDeclarations(
149239
}
150240
}
151241

152-
// Check which imports are needed for interfaces and types (including non-exported ones that are referenced by exported items)
242+
// Check which imports are needed for ALL declarations that will be included in the DTS output
243+
// This includes non-exported types, interfaces, classes, etc. that are still part of the public API
244+
245+
// Check interfaces (both exported and non-exported ones that are referenced)
153246
for (const iface of interfaces) {
154-
// Include interface if it's exported OR if it's referenced by exported functions
247+
// Include interface if it's exported OR if it's referenced by any declaration we're including
155248
const isReferencedByExports = functions.some(func =>
156249
func.isExported && func.text.includes(iface.name),
157250
)
251+
const isReferencedByClasses = classes.some(cls =>
252+
cls.text.includes(iface.name),
253+
)
254+
const isReferencedByTypes = types.some(type =>
255+
type.text.includes(iface.name),
256+
)
158257

159-
if (iface.isExported || isReferencedByExports) {
258+
if (iface.isExported || isReferencedByExports || isReferencedByClasses || isReferencedByTypes) {
160259
for (const imp of imports) {
161260
const allImportedItems = extractAllImportedItems(imp.text)
162261
for (const item of allImportedItems) {
@@ -169,15 +268,53 @@ export function processDeclarations(
169268
}
170269
}
171270

271+
// Check ALL types (exported and non-exported) since they may be included in DTS
172272
for (const type of types) {
173-
if (type.isExported) {
174-
for (const imp of imports) {
175-
const allImportedItems = extractAllImportedItems(imp.text)
176-
for (const item of allImportedItems) {
177-
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
178-
if (regex.test(type.text)) {
179-
usedImportItems.add(item)
180-
}
273+
for (const imp of imports) {
274+
const allImportedItems = extractAllImportedItems(imp.text)
275+
for (const item of allImportedItems) {
276+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
277+
if (regex.test(type.text)) {
278+
usedImportItems.add(item)
279+
}
280+
}
281+
}
282+
}
283+
284+
// Check ALL classes (exported and non-exported) since they may be included in DTS
285+
for (const cls of classes) {
286+
for (const imp of imports) {
287+
const allImportedItems = extractAllImportedItems(imp.text)
288+
for (const item of allImportedItems) {
289+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
290+
if (regex.test(cls.text)) {
291+
usedImportItems.add(item)
292+
}
293+
}
294+
}
295+
}
296+
297+
// Check ALL enums (exported and non-exported) since they may be included in DTS
298+
for (const enumDecl of enums) {
299+
for (const imp of imports) {
300+
const allImportedItems = extractAllImportedItems(imp.text)
301+
for (const item of allImportedItems) {
302+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
303+
if (regex.test(enumDecl.text)) {
304+
usedImportItems.add(item)
305+
}
306+
}
307+
}
308+
}
309+
310+
// Check ALL modules/namespaces since they may be included in DTS
311+
for (const mod of modules) {
312+
for (const imp of imports) {
313+
const allImportedItems = extractAllImportedItems(imp.text)
314+
for (const item of allImportedItems) {
315+
const regex = new RegExp(`\\b${item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
316+
if (regex.test(mod.text)) {
317+
usedImportItems.add(item)
181318
}
182319
}
183320
}
@@ -352,7 +489,13 @@ export function processDeclarations(
352489
// Process default export last
353490
output.push(...defaultExport)
354491

355-
return output.filter(line => line !== '').join('\n')
492+
let result = output.filter(line => line !== '').join('\n')
493+
494+
// Post-process to replace unresolved internal types with 'any'
495+
// This handles cases where internal interfaces/types are referenced but not extracted
496+
result = replaceUnresolvedTypes(result, declarations, imports)
497+
498+
return result
356499
}
357500

358501
/**

0 commit comments

Comments
 (0)