Skip to content

Commit 3ca2cb8

Browse files
committed
fix(bundle): infinite loop due to circular reference
1 parent 47cabfc commit 3ca2cb8

File tree

3 files changed

+54
-24
lines changed

3 files changed

+54
-24
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class User<T extends User = any> {
2+
name: string
3+
4+
constructor(data: T) {
5+
this.name = data.name
6+
}
7+
}

__tests__/bundle-addon.ts

+18
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,24 @@ test('ambient declaration', () => {
8888
expect(bundled.match(/interface InternalInterface {$/gm)).toHaveLength(2)
8989
})
9090

91+
test('circular reference bug', () => {
92+
const entryPoint = fixture('circular-reference-bug', 'dist', 'main.d.ts')
93+
94+
build({
95+
basePath: fixture('circular-reference-bug'),
96+
extends: '../tsconfig.json',
97+
compilerOptions,
98+
clean: { outDir: true },
99+
bundleDeclaration: {
100+
entryPoint,
101+
},
102+
})
103+
104+
const bundled = readFileSync(entryPoint, 'utf8')
105+
106+
expect(bundled).toMatch(/^export declare class User<T extends User = any> {$/m)
107+
})
108+
91109
test.skip('complex', () => {
92110
const entryPoint = fixture('complex', 'dist', 'main.d.ts')
93111

src/bundle-addon/symbol-collector.ts

+29-24
Original file line numberDiff line numberDiff line change
@@ -92,33 +92,18 @@ export class SymbolCollector {
9292
/**
9393
* Retrieves unexported symbols used by exported symbols.
9494
*/
95-
private getReferences(origSymbol: ts.Symbol): Reference[] {
96-
// We look in the first declaration to retrieve common source file,
97-
// since merged and overloaded declarations are in the same source file.
98-
const sourceFile = origSymbol.declarations[0].getSourceFile()
99-
95+
private getReferences(origSymbol: ts.Symbol, symbolsChain: ts.Symbol[] = []): Reference[] {
10096
// Don't search in external symbol declarations.
97+
// We need to check every declaration because of augmentations that could lead to false negatives.
10198
if (
102-
this.program.isSourceFileFromExternalLibrary(sourceFile) ||
103-
this.program.isSourceFileDefaultLibrary(sourceFile)
99+
origSymbol.declarations.some((d) => this.program.isSourceFileFromExternalLibrary(d.getSourceFile())) ||
100+
origSymbol.declarations.some((d) => this.program.isSourceFileDefaultLibrary(d.getSourceFile()))
104101
) {
105102
return []
106103
}
107104

108-
const getSubReferences = (identifier: ts.Identifier): Reference[] => {
109-
let refSymbol = this.checker.getSymbolAtLocation(identifier)
110-
111-
// Avoid infinite loop.
112-
if (!refSymbol || refSymbol === origSymbol) return []
113-
114-
if (refSymbol.flags & ts.SymbolFlags.Alias) {
115-
refSymbol = this.checker.getAliasedSymbol(refSymbol)
116-
}
117-
118-
if (!refSymbol.declarations || !refSymbol.declarations.length) return []
119-
120-
return this.getReferences(refSymbol)
121-
}
105+
// Keep parent symbols in an array to avoid infinite loop when looking for circular subreferences.
106+
symbolsChain.push(origSymbol)
122107

123108
const refs: Reference[] = []
124109

@@ -128,21 +113,41 @@ export class SymbolCollector {
128113

129114
if (ts.isSourceFile(declaration)) continue
130115

131-
// Taken from https://github.com/microsoft/rushstack/blob/2cb32ec198/apps/api-extractor/src/analyzer/AstSymbolTable.ts#L298
116+
const getSubReferences = (identifier: ts.Identifier): Reference[] => {
117+
let refSymbol = this.checker.getSymbolAtLocation(identifier)
118+
119+
// Avoid infinite loop due to circular references.
120+
if (!refSymbol || symbolsChain.includes(refSymbol)) return []
121+
122+
if (refSymbol.flags & ts.SymbolFlags.Alias) {
123+
refSymbol = this.checker.getAliasedSymbol(refSymbol)
124+
}
125+
126+
if (!refSymbol.declarations || !refSymbol.declarations.length) return []
127+
128+
return this.getReferences(refSymbol, symbolsChain)
129+
}
130+
132131
declaration.forEachChild(function visit(child) {
132+
// Taken from https://github.com/microsoft/rushstack/blob/2cb32ec198/apps/api-extractor/src/analyzer/AstSymbolTable.ts#L298
133133
if (
134134
ts.isTypeReferenceNode(child) ||
135135
ts.isExpressionWithTypeArguments(child) || // "extends"
136136
ts.isComputedPropertyName(child) || // [Prop]:
137137
ts.isTypeQueryNode(child) // "typeof X"
138138
) {
139139
const identifier = findFirstChild(child, ts.isIdentifier)
140+
140141
if (identifier) {
141-
refs.push({ ref: identifier, subrefs: getSubReferences(identifier), declarationIndex })
142+
const subrefs = getSubReferences(identifier)
143+
144+
refs.push({ ref: identifier, subrefs, declarationIndex })
142145
}
143146
}
144147

145-
if (ts.isImportTypeNode(child)) refs.push({ ref: child, subrefs: [], declarationIndex })
148+
if (ts.isImportTypeNode(child)) {
149+
refs.push({ ref: child, subrefs: [], declarationIndex })
150+
}
146151

147152
child.forEachChild(visit)
148153
})

0 commit comments

Comments
 (0)