fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3291 from hey-api/copilot/fix-duplicate-import-error

authored by

Lubos and committed by
GitHub
caabc9f9 124dc280

+282 -3
+5
.changeset/selfish-seas-add.md
··· 1 + --- 2 + "@hey-api/codegen-core": patch 3 + --- 4 + 5 + **planner**: fix duplicate import when same symbol is imported as both type and value
+275
packages/codegen-core/src/__tests__/planner.test.ts
··· 1 + import type { INode } from '../nodes/node'; 2 + import { Project } from '../project/project'; 3 + import { ref } from '../refs/refs'; 4 + import type { Symbol } from '../symbols/symbol'; 5 + import type { SymbolKind } from '../symbols/types'; 6 + 7 + /** 8 + * Creates a mock node for testing. 9 + */ 10 + const createMockNode = (args: { 11 + dependencies?: Array<Symbol>; 12 + filePath: string; 13 + language?: 'typescript' | 'javascript'; 14 + name: string; 15 + project: Project; 16 + symbolKind?: SymbolKind; 17 + }): { node: INode; symbol: Symbol } => { 18 + const { 19 + dependencies = [], 20 + filePath, 21 + language = 'typescript', 22 + name, 23 + project, 24 + symbolKind = 'var', 25 + } = args; 26 + 27 + const symbol = project.symbols.register({ 28 + exported: true, 29 + getFilePath: () => filePath, 30 + kind: symbolKind, 31 + name, 32 + }); 33 + 34 + const node: INode = { 35 + analyze: (ctx) => { 36 + for (const dep of dependencies) { 37 + ctx.addDependency(ref(dep)); 38 + } 39 + }, 40 + clone () { 41 + return this; 42 + }, 43 + exported: true, 44 + language, 45 + name: ref(name) as INode['name'], 46 + scope: 'value', 47 + symbol, 48 + toAst: () => ({}), 49 + '~brand': 'test-node', 50 + }; 51 + 52 + symbol.setNode(node); 53 + project.nodes.add(node); 54 + 55 + return { node, symbol }; 56 + }; 57 + 58 + describe('Planner imports deduplication', () => { 59 + it('produces a single value import when there is 1 imported symbol as value', () => { 60 + const project = new Project({ root: '/root' }); 61 + 62 + // Create source file with exported symbol 63 + const { symbol: sourceSymbol } = createMockNode({ 64 + filePath: 'source', 65 + name: 'MyValue', 66 + project, 67 + symbolKind: 'var', 68 + }); 69 + 70 + // Create consumer file that imports the source symbol as value 71 + createMockNode({ 72 + dependencies: [sourceSymbol], 73 + filePath: 'consumer', 74 + name: 'Consumer', 75 + project, 76 + }); 77 + 78 + project.plan(); 79 + 80 + const consumerFile = [...project.files.registered()].find((f) => f.name === 'consumer'); 81 + expect(consumerFile).toBeDefined(); 82 + 83 + const imports = consumerFile!.imports; 84 + expect(imports).toHaveLength(1); 85 + expect(imports[0]!.imports).toHaveLength(1); 86 + expect(imports[0]!.imports[0]!.isTypeOnly).toBe(false); 87 + expect(imports[0]!.imports[0]!.localName).toBe('MyValue'); 88 + }); 89 + 90 + it('produces a single type import when there is 1 imported symbol as type', () => { 91 + const project = new Project({ root: '/root' }); 92 + 93 + // Create source file with exported type symbol 94 + const { symbol: sourceSymbol } = createMockNode({ 95 + filePath: 'source', 96 + name: 'MyType', 97 + project, 98 + symbolKind: 'type', 99 + }); 100 + 101 + // Create consumer file that imports the source symbol as type 102 + createMockNode({ 103 + dependencies: [sourceSymbol], 104 + filePath: 'consumer', 105 + name: 'Consumer', 106 + project, 107 + }); 108 + 109 + project.plan(); 110 + 111 + const consumerFile = [...project.files.registered()].find((f) => f.name === 'consumer'); 112 + expect(consumerFile).toBeDefined(); 113 + 114 + const imports = consumerFile!.imports; 115 + expect(imports).toHaveLength(1); 116 + expect(imports[0]!.imports).toHaveLength(1); 117 + expect(imports[0]!.imports[0]!.isTypeOnly).toBe(true); 118 + expect(imports[0]!.imports[0]!.localName).toBe('MyType'); 119 + }); 120 + 121 + it('produces a single value import when there are 2 imported symbols as values with same name', () => { 122 + const project = new Project({ root: '/root' }); 123 + 124 + // Create source file with exported symbol 125 + const { symbol: sourceSymbol } = createMockNode({ 126 + filePath: 'source', 127 + name: 'MyValue', 128 + project, 129 + symbolKind: 'var', 130 + }); 131 + 132 + // Create consumer file that imports the source symbol twice as value 133 + const consumerSymbol = project.symbols.register({ 134 + exported: true, 135 + getFilePath: () => 'consumer', 136 + kind: 'var', 137 + name: 'Consumer', 138 + }); 139 + const consumerNode: INode = { 140 + analyze: (ctx) => { 141 + ctx.addDependency(ref(sourceSymbol)); 142 + ctx.addDependency(ref(sourceSymbol)); 143 + }, 144 + clone () { 145 + return this; 146 + }, 147 + exported: true, 148 + language: 'typescript', 149 + name: ref('Consumer') as INode['name'], 150 + scope: 'value', 151 + symbol: consumerSymbol, 152 + toAst: () => ({}), 153 + '~brand': 'test-node', 154 + }; 155 + consumerSymbol.setNode(consumerNode); 156 + project.nodes.add(consumerNode); 157 + 158 + project.plan(); 159 + 160 + const consumerFile = [...project.files.registered()].find((f) => f.name === 'consumer'); 161 + expect(consumerFile).toBeDefined(); 162 + 163 + const imports = consumerFile!.imports; 164 + expect(imports).toHaveLength(1); 165 + expect(imports[0]!.imports).toHaveLength(1); 166 + expect(imports[0]!.imports[0]!.isTypeOnly).toBe(false); 167 + expect(imports[0]!.imports[0]!.localName).toBe('MyValue'); 168 + }); 169 + 170 + it('produces a single type import when there are 2 imported symbols as types with same name', () => { 171 + const project = new Project({ root: '/root' }); 172 + 173 + // Create source file with exported type symbol 174 + const { symbol: sourceSymbol } = createMockNode({ 175 + filePath: 'source', 176 + name: 'MyType', 177 + project, 178 + symbolKind: 'type', 179 + }); 180 + 181 + // Create consumer file that imports the source symbol twice as type 182 + const consumerSymbol = project.symbols.register({ 183 + exported: true, 184 + getFilePath: () => 'consumer', 185 + kind: 'var', 186 + name: 'Consumer', 187 + }); 188 + const consumerNode: INode = { 189 + analyze: (ctx) => { 190 + ctx.addDependency(ref(sourceSymbol)); 191 + ctx.addDependency(ref(sourceSymbol)); 192 + }, 193 + clone () { 194 + return this; 195 + }, 196 + exported: true, 197 + language: 'typescript', 198 + name: ref('Consumer') as INode['name'], 199 + scope: 'value', 200 + symbol: consumerSymbol, 201 + toAst: () => ({}), 202 + '~brand': 'test-node', 203 + }; 204 + consumerSymbol.setNode(consumerNode); 205 + project.nodes.add(consumerNode); 206 + 207 + project.plan(); 208 + 209 + const consumerFile = [...project.files.registered()].find((f) => f.name === 'consumer'); 210 + expect(consumerFile).toBeDefined(); 211 + 212 + const imports = consumerFile!.imports; 213 + expect(imports).toHaveLength(1); 214 + expect(imports[0]!.imports).toHaveLength(1); 215 + expect(imports[0]!.imports[0]!.isTypeOnly).toBe(true); 216 + expect(imports[0]!.imports[0]!.localName).toBe('MyType'); 217 + }); 218 + 219 + it('produces a single value import when there are 2 imported symbols, 1 as type and 1 as value, with same name', () => { 220 + const project = new Project({ root: '/root' }); 221 + 222 + // Create source file with exported type symbol 223 + const { symbol: typeSymbol } = createMockNode({ 224 + filePath: 'source', 225 + name: 'MySymbol', 226 + project, 227 + symbolKind: 'type', 228 + }); 229 + 230 + // Create source file with exported value symbol with same name 231 + const { symbol: valueSymbol } = createMockNode({ 232 + filePath: 'source', 233 + name: 'MySymbol', 234 + project, 235 + symbolKind: 'var', 236 + }); 237 + 238 + // Create consumer file that imports both symbols (type and value with same name) 239 + const consumerSymbol = project.symbols.register({ 240 + exported: true, 241 + getFilePath: () => 'consumer', 242 + kind: 'var', 243 + name: 'Consumer', 244 + }); 245 + const consumerNode: INode = { 246 + analyze: (ctx) => { 247 + ctx.addDependency(ref(typeSymbol)); 248 + ctx.addDependency(ref(valueSymbol)); 249 + }, 250 + clone () { 251 + return this; 252 + }, 253 + exported: true, 254 + language: 'typescript', 255 + name: ref('Consumer') as INode['name'], 256 + scope: 'value', 257 + symbol: consumerSymbol, 258 + toAst: () => ({}), 259 + '~brand': 'test-node', 260 + }; 261 + consumerSymbol.setNode(consumerNode); 262 + project.nodes.add(consumerNode); 263 + 264 + project.plan(); 265 + 266 + const consumerFile = [...project.files.registered()].find((f) => f.name === 'consumer'); 267 + expect(consumerFile).toBeDefined(); 268 + 269 + const imports = consumerFile!.imports; 270 + expect(imports).toHaveLength(1); 271 + expect(imports[0]!.imports).toHaveLength(1); 272 + expect(imports[0]!.imports[0]!.isTypeOnly).toBe(false); 273 + expect(imports[0]!.imports[0]!.localName).toBe('MySymbol'); 274 + }); 275 + });
+2 -3
packages/codegen-core/src/planner/planner.ts
··· 266 266 267 267 const fromFileId = dep.file.id; 268 268 const importedName = dep.finalName; 269 - const isTypeOnly = isTypeOnlyKind(dep.kind); 270 269 const kind = dep.importKind; 271 - const key = `${fromFileId}|${importedName}|${kind}|${isTypeOnly}`; 270 + const key = `${fromFileId}|${importedName}|${kind}`; 272 271 273 272 let entry = fileMap.get(key); 274 273 if (!entry) { ··· 292 291 symbol: imp, 293 292 }; 294 293 fileMap.set(key, entry); 295 - entry.kinds.add(imp.kind); 296 294 } 295 + entry.kinds.add(dep.kind); 297 296 298 297 dependency['~ref'] = entry.symbol; 299 298 });