fork of hey-api/openapi-ts because I need some additional things
1import { symbolBrand } from '../brands';
2import type { ISymbolMeta } from '../extensions';
3import type { File } from '../files/file';
4import { log } from '../log';
5import type { INode } from '../nodes/node';
6import type { BindingKind, ISymbolIn, SymbolKind } from './types';
7
8export class Symbol<Node extends INode = INode> {
9 /**
10 * Canonical symbol this stub resolves to, if any.
11 *
12 * Stubs created during DSL construction may later be associated
13 * with a fully registered symbol. Once set, all property lookups
14 * should defer to the canonical symbol.
15 */
16 private _canonical?: Symbol;
17 /**
18 * True if this symbol is exported from its defining file.
19 *
20 * @default false
21 */
22 private _exported: boolean;
23 /**
24 * External module name if this symbol is imported from a module not managed
25 * by the project (e.g., "zod", "lodash").
26 *
27 * @default undefined
28 */
29 private _external?: string;
30 /**
31 * The file this symbol is ultimately emitted into.
32 *
33 * Only top-level symbols have an assigned file.
34 */
35 private _file?: File;
36 /**
37 * The alias-resolved, conflict-free emitted name.
38 */
39 private _finalName?: string;
40 /**
41 * Custom strategy to determine from which file path(s) this symbol is re-exported.
42 *
43 * @returns The file path(s) that re-export this symbol, or undefined if none.
44 */
45 private _getExportFromFilePath?: (symbol: Symbol) => ReadonlyArray<string> | undefined;
46 /**
47 * Custom strategy to determine file output path.
48 *
49 * @returns The file path to output the symbol to, or undefined to fallback to default behavior.
50 */
51 private _getFilePath?: (symbol: Symbol) => string | undefined;
52 /**
53 * How this symbol should be imported (namespace/default/named).
54 *
55 * @default 'named'
56 */
57 private _importKind: BindingKind;
58 /**
59 * Kind of symbol (class, type, alias, etc.).
60 *
61 * @default 'var'
62 */
63 private _kind: SymbolKind;
64 /**
65 * Arbitrary user metadata.
66 *
67 * @default undefined
68 */
69 private _meta?: ISymbolMeta;
70 /**
71 * Intended user-facing name before conflict resolution.
72 *
73 * @example "UserModel"
74 */
75 private _name: string;
76 /**
77 * Node that defines this symbol.
78 */
79 private _node?: Node;
80
81 /** Brand used for identifying symbols. */
82 readonly '~brand' = symbolBrand;
83 /** Globally unique, stable symbol ID. */
84 readonly id: number;
85
86 constructor(input: ISymbolIn, id: number) {
87 this._exported = input.exported ?? false;
88 this._external = input.external;
89 this._getExportFromFilePath = input.getExportFromFilePath;
90 this._getFilePath = input.getFilePath;
91 this.id = id;
92 this._importKind = input.importKind ?? 'named';
93 this._kind = input.kind ?? 'var';
94 this._meta = input.meta;
95 this._name = input.name;
96 }
97
98 /**
99 * Returns the canonical symbol for this instance.
100 *
101 * If this symbol was created as a stub, this getter returns
102 * the fully registered canonical symbol. Otherwise, it returns
103 * the symbol itself.
104 */
105 get canonical(): Symbol {
106 return this._canonical ?? this;
107 }
108
109 /**
110 * Indicates whether this symbol is exported from its defining file.
111 */
112 get exported(): boolean {
113 return this.canonical._exported;
114 }
115
116 /**
117 * External module from which this symbol originates, if any.
118 */
119 get external(): string | undefined {
120 return this.canonical._external;
121 }
122
123 /**
124 * Read‑only accessor for the assigned output file.
125 *
126 * Only top-level symbols have an assigned file.
127 */
128 get file(): File | undefined {
129 return this.canonical._file;
130 }
131
132 /**
133 * Read‑only accessor for the resolved final emitted name.
134 */
135 get finalName(): string {
136 if (!this.canonical._finalName) {
137 const message = `Symbol finalName has not been resolved yet for ${this.canonical.toString()}`;
138 log.debug(message, 'symbol');
139 throw new Error(message);
140 }
141 return this.canonical._finalName;
142 }
143
144 /**
145 * Custom re-export file path resolver, if provided.
146 */
147 get getExportFromFilePath(): ((symbol: Symbol) => ReadonlyArray<string> | undefined) | undefined {
148 return this.canonical._getExportFromFilePath;
149 }
150
151 /**
152 * Custom file path resolver, if provided.
153 */
154 get getFilePath(): ((symbol: Symbol) => string | undefined) | undefined {
155 return this.canonical._getFilePath;
156 }
157
158 /**
159 * How this symbol should be imported (named/default/namespace).
160 */
161 get importKind(): BindingKind {
162 return this.canonical._importKind;
163 }
164
165 /**
166 * Indicates whether this is a canonical symbol (not a stub).
167 */
168 get isCanonical(): boolean {
169 return !this._canonical || this._canonical === this;
170 }
171
172 /**
173 * The symbol's kind (class, type, alias, variable, etc.).
174 */
175 get kind(): SymbolKind {
176 return this.canonical._kind;
177 }
178
179 /**
180 * Arbitrary user‑provided metadata associated with this symbol.
181 */
182 get meta(): ISymbolMeta | undefined {
183 return this.canonical._meta;
184 }
185
186 /**
187 * User-intended name before aliasing or conflict resolution.
188 */
189 get name(): string {
190 return this.canonical._name;
191 }
192
193 /**
194 * Read‑only accessor for the defining node.
195 */
196 get node(): Node | undefined {
197 return this.canonical._node as Node | undefined;
198 }
199
200 /**
201 * Marks this symbol as a stub and assigns its canonical symbol.
202 *
203 * After calling this, all semantic queries (name, kind, file,
204 * meta, etc.) should reflect the canonical symbol's values.
205 *
206 * @param symbol — The canonical symbol this stub should resolve to.
207 */
208 setCanonical(symbol: Symbol): void {
209 this._canonical = symbol;
210 }
211
212 /**
213 * Marks the symbol as exported from its file.
214 *
215 * @param exported — Whether the symbol is exported.
216 */
217 setExported(exported: boolean): void {
218 this.assertCanonical();
219 this._exported = exported;
220 }
221
222 /**
223 * Assigns the output file this symbol will be emitted into.
224 *
225 * This may only be set once.
226 */
227 setFile(file: File): void {
228 this.assertCanonical();
229 if (this._file && this._file !== file) {
230 const message = `Symbol ${this.canonical.toString()} is already assigned to a different file.`;
231 log.debug(message, 'symbol');
232 throw new Error(message);
233 }
234 this._file = file;
235 }
236
237 /**
238 * Assigns the conflict‑resolved final local name for this symbol.
239 *
240 * This may only be set once.
241 */
242 setFinalName(name: string): void {
243 this.assertCanonical();
244 if (this._finalName && this._finalName !== name) {
245 const message = `Symbol finalName has already been resolved for ${this.canonical.toString()}.`;
246 log.debug(message, 'symbol');
247 throw new Error(message);
248 }
249 this._finalName = name;
250 }
251
252 /**
253 * Sets how this symbol should be imported.
254 *
255 * @param kind — The import strategy (named/default/namespace).
256 */
257 setImportKind(kind: BindingKind): void {
258 this.assertCanonical();
259 this._importKind = kind;
260 }
261
262 /**
263 * Sets the symbol's kind (class, type, alias, variable, etc.).
264 *
265 * @param kind — The new symbol kind.
266 */
267 setKind(kind: SymbolKind): void {
268 this.assertCanonical();
269 this._kind = kind;
270 }
271
272 /**
273 * Updates the intended user‑facing name for this symbol.
274 *
275 * @param name — The new name.
276 */
277 setName(name: string): void {
278 this.assertCanonical();
279 this._name = name;
280 }
281
282 /**
283 * Binds the node that defines this symbol.
284 *
285 * This may only be set once.
286 */
287 setNode(node: Node): void {
288 this.assertCanonical();
289 if (this._node && this._node !== node) {
290 const message = `Symbol ${this.canonical.toString()} is already bound to a different node.`;
291 log.debug(message, 'symbol');
292 // TODO: symbol <> node relationship needs to be refactor to 1:many
293 // disabled in the meantime or it would throw
294 // throw new Error(message);
295 }
296 this._node = node;
297 node.symbol = this;
298 }
299
300 /**
301 * Returns a debug‑friendly string representation identifying the symbol.
302 */
303 toString(): string {
304 const canonical = this.canonical;
305 if (canonical._finalName && canonical._finalName !== canonical._name) {
306 return `[Symbol ${canonical._name} → ${canonical._finalName}#${canonical.id}]`;
307 }
308 return `[Symbol ${canonical._name || canonical._meta !== undefined ? JSON.stringify(canonical._meta) : '<unknown>'}#${canonical.id}]`;
309 }
310
311 /**
312 * Ensures this symbol is canonical before allowing mutation.
313 *
314 * A symbol that has been marked as a stub (i.e., its `_canonical` points
315 * to a different symbol) may not be mutated. This guard throws an error
316 * if any setter attempts to modify a stub, preventing accidental writes
317 * to non‑canonical instances.
318 *
319 * @throws {Error} If the symbol is a stub and is being mutated.
320 */
321 private assertCanonical(): void {
322 if (this._canonical && this._canonical !== this) {
323 const message = `Illegal mutation of stub symbol ${this.toString()} → canonical: ${this._canonical.toString()}`;
324 log.debug(message, 'symbol');
325 throw new Error(message);
326 }
327 }
328}