fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 231 lines 6.7 kB view raw
1import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2import type { MaybeArray } from '@hey-api/types'; 3 4import { py } from '../../ts-python'; 5import type { MaybePyDsl } from '../base'; 6import { PyDsl } from '../base'; 7import type { DoExpr } from '../mixins/do'; 8import { BlockPyDsl } from './block'; 9 10const Mixed = PyDsl<py.TryStatement>; 11 12type ExceptType = string | MaybePyDsl<py.Expression>; 13 14interface ExceptEntry { 15 body: Array<DoExpr>; 16 name?: NodeName; 17 types: Array<ExceptType>; 18} 19 20function exceptKey(types: Array<ExceptType>): string { 21 return types 22 .map((t) => (typeof t === 'string' ? t : '<<expr>>')) 23 .sort() 24 .join(','); 25} 26 27export class TryPyDsl extends Mixed { 28 readonly '~dsl' = 'TryPyDsl'; 29 30 /** 31 * Ordered list of except clauses. We also keep a lookup map 32 * (`_exceptIndex`) keyed by the normalised type key so that 33 * repeated `.except()` calls with the same type set merge their 34 * body statements instead of creating duplicate clauses. 35 */ 36 protected _excepts: Array<ExceptEntry> = []; 37 protected _exceptIndex: Map<string, number> = new Map(); 38 39 protected _finally?: Array<DoExpr>; 40 protected _try?: Array<DoExpr>; 41 42 constructor(...tryBlock: Array<DoExpr>) { 43 super(); 44 this.try(...tryBlock); 45 } 46 47 override analyze(ctx: AnalysisContext): void { 48 super.analyze(ctx); 49 50 if (this._try) { 51 ctx.pushScope(); 52 try { 53 for (const stmt of this._try) ctx.analyze(stmt); 54 } finally { 55 ctx.popScope(); 56 } 57 } 58 59 for (const entry of this._excepts) { 60 ctx.pushScope(); 61 try { 62 ctx.analyze(entry.name); 63 for (const t of entry.types) ctx.analyze(t); 64 for (const stmt of entry.body) ctx.analyze(stmt); 65 } finally { 66 ctx.popScope(); 67 } 68 } 69 70 if (this._finally) { 71 ctx.pushScope(); 72 try { 73 for (const stmt of this._finally) ctx.analyze(stmt); 74 } finally { 75 ctx.popScope(); 76 } 77 } 78 } 79 80 /** Returns true when all required builder calls are present. */ 81 get isValid(): boolean { 82 return this.missingRequiredCalls().length === 0; 83 } 84 85 /** 86 * Add (or merge into) an except clause. 87 * 88 * ```ts 89 * $.try(...) 90 * .except('ValueError', 'e', body1, body2) // except ValueError as e: 91 * .except(['TypeError', 'KeyError'], 'e', ...) // except (TypeError, KeyError) as e: 92 * .except('ValueError', moreBody) // merges into first clause 93 * ``` 94 * 95 * @param types Single exception type or array of types. 96 * @param nameOrBody Either the `as` variable name (`NodeName`) or the 97 * first body expression. If it looks like a `NodeName` (string that 98 * is a valid Python identifier and is *not* a DSL node), it is treated 99 * as the name; pass body items after it. 100 * @param body Remaining body statements. 101 */ 102 except( 103 types: MaybeArray<ExceptType>, 104 nameOrBody?: NodeName | DoExpr, 105 ...body: Array<DoExpr> 106 ): this { 107 const typeArr = Array.isArray(types) ? types : [types]; 108 const key = exceptKey(typeArr); 109 110 let name: NodeName | undefined; 111 let bodyItems: Array<DoExpr>; 112 113 // Disambiguate: if the second arg is a plain string that looks like 114 // an identifier (no dots, no spaces, not a DSL node) treat it as 115 // the `as` name. Otherwise it's the first body expression. 116 if (nameOrBody !== undefined && this._isNodeName(nameOrBody)) { 117 name = nameOrBody as NodeName; 118 bodyItems = body; 119 } else if (nameOrBody !== undefined) { 120 bodyItems = [nameOrBody as DoExpr, ...body]; 121 } else { 122 bodyItems = body; 123 } 124 125 const existing = this._exceptIndex.get(key); 126 if (existing !== undefined) { 127 const entry = this._excepts[existing]!; 128 entry.body.push(...bodyItems); 129 if (name !== undefined) entry.name = name; 130 } else { 131 this._exceptIndex.set(key, this._excepts.length); 132 this._excepts.push({ body: bodyItems, name, types: typeArr }); 133 } 134 135 return this; 136 } 137 138 /** Add a bare `except:` clause (catches everything). */ 139 exceptAll(...body: Array<DoExpr>): this { 140 const key = ''; 141 const existing = this._exceptIndex.get(key); 142 if (existing !== undefined) { 143 this._excepts[existing]!.body.push(...body); 144 } else { 145 this._exceptIndex.set(key, this._excepts.length); 146 this._excepts.push({ body, types: [] }); 147 } 148 return this; 149 } 150 151 finally(...items: Array<DoExpr>): this { 152 this._finally = items; 153 return this; 154 } 155 156 try(...items: Array<DoExpr>): this { 157 this._try = items; 158 return this; 159 } 160 161 override toAst() { 162 this.$validate(); 163 164 const tryStatements = new BlockPyDsl(...this._try!).$do(); 165 166 let exceptClauses: Array<py.ExceptClause> | undefined; 167 if (this._excepts.length) { 168 exceptClauses = this._excepts.map((entry) => { 169 const bodyStatements = new BlockPyDsl(...entry.body).$do(); 170 171 let exceptionType: py.Expression | undefined; 172 if (entry.types.length === 1) { 173 exceptionType = this.$node(entry.types[0]!); 174 } else if (entry.types.length > 1) { 175 exceptionType = py.factory.createTupleExpression( 176 entry.types.map((t) => this.$node(t) as py.Expression), 177 ); 178 } 179 180 const exceptionName = entry.name 181 ? py.factory.createIdentifier( 182 this.$name({ current: entry.name } as any) || String(entry.name), 183 ) 184 : undefined; 185 186 return py.factory.createExceptClause([...bodyStatements], exceptionType, exceptionName); 187 }); 188 } 189 190 const finallyStatements = this._finally 191 ? [...new BlockPyDsl(...this._finally).$do()] 192 : undefined; 193 194 return py.factory.createTryStatement( 195 [...tryStatements], 196 exceptClauses, 197 undefined, 198 finallyStatements, 199 ); 200 } 201 202 $validate(): asserts this is this & { 203 _try: Array<DoExpr>; 204 } { 205 const missing = this.missingRequiredCalls(); 206 if (missing.length === 0) return; 207 throw new Error(`Try statement missing ${missing.join(' and ')}`); 208 } 209 210 private missingRequiredCalls(): ReadonlyArray<string> { 211 const missing: Array<string> = []; 212 if (!this._try || this._try.length === 0) missing.push('.try()'); 213 return missing; 214 } 215 216 /** 217 * Heuristic: a value is a `NodeName` (intended as the `as` variable) 218 * if it is a plain string matching a Python identifier pattern, or a 219 * Symbol. 220 */ 221 private _isNodeName(value: unknown): boolean { 222 if (typeof value === 'string') { 223 return /^[A-Za-z_]\w*$/.test(value); 224 } 225 // Symbols from codegen-core have `~brand` 226 if (value && typeof value === 'object' && '~brand' in value) { 227 return true; 228 } 229 return false; 230 } 231}