|
1 | | -import type { Analysis as AnyAnalysis, Analyzer } from "./model.js"; |
2 | | -import { Analysis } from "./model.js"; |
| 1 | +import { fromThrowable, type Result } from "../neverthrow/index.js"; |
| 2 | +import { Analysis, AnalysisError, Analyzer, AnalyzedData } from "./model.js"; |
| 3 | + |
| 4 | +export class ArchiverNotRecognizedError extends AnalysisError { |
| 5 | + readonly kind = "archiver-not-recognized" as const; |
| 6 | +} |
| 7 | + |
| 8 | +export class ArchiverUnsupportedError extends AnalysisError { |
| 9 | + readonly kind = "archiver-unsupported" as const; |
| 10 | +} |
| 11 | + |
| 12 | +export class ArchiverParseError extends AnalysisError { |
| 13 | + readonly kind = "archiver-parse" as const; |
| 14 | +} |
| 15 | + |
| 16 | +export type ArchiverAnalysisError = |
| 17 | + | ArchiverNotRecognizedError |
| 18 | + | ArchiverUnsupportedError |
| 19 | + | ArchiverParseError; |
| 20 | + |
| 21 | +function toArchiverAnalysisError( |
| 22 | + value: unknown, |
| 23 | + context: string, |
| 24 | +): ArchiverAnalysisError { |
| 25 | + if ( |
| 26 | + value instanceof ArchiverNotRecognizedError || |
| 27 | + value instanceof ArchiverUnsupportedError || |
| 28 | + value instanceof ArchiverParseError |
| 29 | + ) { |
| 30 | + return value; |
| 31 | + } |
| 32 | + |
| 33 | + if (value instanceof Error) { |
| 34 | + return new ArchiverParseError(`${context}: ${value.message}`); |
| 35 | + } |
| 36 | + |
| 37 | + return new ArchiverParseError(`${context}: ${String(value)}`); |
| 38 | +} |
3 | 39 |
|
4 | 40 | /** |
5 | 41 | * Supported single-letter archive operations. |
@@ -41,16 +77,15 @@ export type ArchiverOperation = |
41 | 77 | */ |
42 | 78 | export type ArchiverExe = "ar" | "llvm-ar" | "gcc-ar"; |
43 | 79 |
|
44 | | -type ArchiverModel = { |
45 | | - exe: ArchiverExe; |
| 80 | +export type ArchiverModel = { |
46 | 81 | operation: ArchiverOperation; |
47 | 82 | modifiers: string[]; |
48 | 83 | thin: boolean; |
49 | 84 | archive?: string; |
50 | 85 | members: string[]; |
51 | 86 | scriptMode: boolean; |
52 | | - consume: string[]; |
53 | | - produce: string[]; |
| 87 | + reads: string[]; |
| 88 | + writes: string[]; |
54 | 89 | }; |
55 | 90 |
|
56 | 91 | const ARCHIVER_EXE_NAMES = new Set<ArchiverExe>(["ar", "llvm-ar", "gcc-ar"]); |
@@ -139,25 +174,28 @@ function parseOperationToken( |
139 | 174 | }; |
140 | 175 | } |
141 | 176 |
|
142 | | -function analyzeArchiverModel( |
143 | | - cmd: readonly string[], |
144 | | -): ArchiverModel | undefined { |
145 | | - if (cmd.length === 0) { |
146 | | - return undefined; |
| 177 | +function analyzeArchiverModel(command: AnalyzedData): ArchiverModel { |
| 178 | + if (command.argv.length === 0) { |
| 179 | + throw new ArchiverNotRecognizedError("empty argv"); |
147 | 180 | } |
148 | 181 |
|
149 | | - const exe = exeStem(cmd[0]); |
| 182 | + const exe = exeStem(command.exe); |
150 | 183 | if (!isArchiverExe(exe)) { |
151 | | - return undefined; |
| 184 | + throw new ArchiverNotRecognizedError( |
| 185 | + `not a recognized archiver executable: ${command.exe}`, |
| 186 | + ); |
152 | 187 | } |
153 | 188 |
|
| 189 | + const cmd = command.argv; |
154 | 190 | let index = 1; |
155 | 191 | let thin = false; |
156 | 192 |
|
157 | 193 | while (index < cmd.length) { |
158 | 194 | const token = cmd[index]; |
159 | 195 | if (token === "-M") { |
160 | | - return undefined; |
| 196 | + throw new ArchiverUnsupportedError( |
| 197 | + "archiver MRI script mode is not supported", |
| 198 | + ); |
161 | 199 | } |
162 | 200 | if (token === "--thin" || token === "-T") { |
163 | 201 | thin = true; |
@@ -192,145 +230,108 @@ function analyzeArchiverModel( |
192 | 230 | continue; |
193 | 231 | } |
194 | 232 | if (isOptionToken(token)) { |
195 | | - return undefined; |
| 233 | + throw new ArchiverUnsupportedError( |
| 234 | + `unsupported archiver option: ${token}`, |
| 235 | + ); |
196 | 236 | } |
197 | 237 | break; |
198 | 238 | } |
199 | 239 |
|
200 | 240 | if (index >= cmd.length) { |
201 | | - return undefined; |
| 241 | + throw new ArchiverUnsupportedError( |
| 242 | + "archiver command has no operation token", |
| 243 | + ); |
202 | 244 | } |
203 | 245 |
|
204 | 246 | const parsedOperation = parseOperationToken(cmd[index]); |
205 | 247 | if (parsedOperation === undefined) { |
206 | | - return undefined; |
| 248 | + throw new ArchiverUnsupportedError( |
| 249 | + `unsupported archiver operation token: ${cmd[index]}`, |
| 250 | + ); |
207 | 251 | } |
208 | 252 | ++index; |
209 | 253 |
|
210 | 254 | const archive = cmd[index]; |
211 | 255 | if (archive === undefined) { |
212 | | - return undefined; |
| 256 | + throw new ArchiverUnsupportedError("archiver command has no archive path"); |
213 | 257 | } |
214 | 258 | ++index; |
215 | 259 |
|
216 | 260 | if (!MODELED_ARCHIVER_OPERATIONS.has(parsedOperation.operation)) { |
217 | | - return undefined; |
| 261 | + throw new ArchiverUnsupportedError( |
| 262 | + `archiver operation is not modeled: ${parsedOperation.operation}`, |
| 263 | + ); |
218 | 264 | } |
219 | 265 |
|
220 | 266 | const members = cmd.slice(index); |
221 | | - const produce = |
| 267 | + const writes = |
222 | 268 | parsedOperation.operation === ArchiverOperation.QuickAppend || |
223 | 269 | parsedOperation.operation === ArchiverOperation.ReplaceOrInsert |
224 | 270 | ? [archive] |
225 | 271 | : []; |
226 | | - const consume = |
| 272 | + const reads = |
227 | 273 | parsedOperation.operation === ArchiverOperation.Print || |
228 | 274 | parsedOperation.operation === ArchiverOperation.Table |
229 | 275 | ? [archive] |
230 | 276 | : [...members]; |
231 | 277 |
|
232 | 278 | return { |
233 | | - exe, |
234 | 279 | operation: parsedOperation.operation, |
235 | 280 | modifiers: parsedOperation.modifiers, |
236 | 281 | thin, |
237 | 282 | archive, |
238 | 283 | members: [...members], |
239 | 284 | scriptMode: false, |
240 | | - consume, |
241 | | - produce, |
| 285 | + reads, |
| 286 | + writes, |
242 | 287 | }; |
243 | 288 | } |
244 | 289 |
|
245 | | -export class ArchiverAnalysis extends Analysis<ArchiverExe> { |
246 | | - /** |
247 | | - * Stable registry key for the archiver analyzer. |
248 | | - * |
249 | | - * @example |
250 | | - * ```ts |
251 | | - * cmd.defaultRegistry.unregister(cmd.ArchiverAnalysis.key); |
252 | | - * ``` |
253 | | - */ |
254 | | - static readonly key = "archiver"; |
255 | | - |
256 | | - /** |
257 | | - * Checks whether a command looks like a supported archiver invocation. |
258 | | - * |
259 | | - * @example |
260 | | - * ```ts |
261 | | - * const ok = cmd.ArchiverAnalysis.supports(["llvm-ar", "rcs", "liba.a", "a.o"]); |
262 | | - * ``` |
263 | | - */ |
264 | | - static supports(cmd: readonly string[]): boolean { |
265 | | - return analyzeArchiverModel(cmd) !== undefined; |
266 | | - } |
267 | | - |
268 | | - /** |
269 | | - * Analyzes an archiver command. |
270 | | - * |
271 | | - * @example |
272 | | - * ```ts |
273 | | - * const analysis = cmd.ArchiverAnalysis.analyze([ |
274 | | - * "llvm-ar", |
275 | | - * "rcs", |
276 | | - * "liba.a", |
277 | | - * "a.o", |
278 | | - * ]); |
279 | | - * ``` |
280 | | - */ |
281 | | - static analyze(cmd: readonly string[]): ArchiverAnalysis | undefined { |
282 | | - return ArchiverAnalysis.supports(cmd) |
283 | | - ? new ArchiverAnalysis(cmd) |
284 | | - : undefined; |
285 | | - } |
286 | | - |
287 | | - /** |
288 | | - * Narrows a generic analysis back to an archiver analysis. |
289 | | - * |
290 | | - * @example |
291 | | - * ```ts |
292 | | - * const analysis = cmd.ArchiverAnalysis.from(cmd.analyze(["ar", "rcs", "liba.a", "a.o"])); |
293 | | - * ``` |
294 | | - */ |
295 | | - static from(analysis: AnyAnalysis | undefined): ArchiverAnalysis | undefined { |
296 | | - return analysis instanceof ArchiverAnalysis ? analysis : undefined; |
297 | | - } |
298 | | - |
| 290 | +export class ArchiverAnalysis extends Analysis { |
| 291 | + /** Discriminator for command analysis unions. */ |
| 292 | + readonly kind = "archiver" as const; |
299 | 293 | /** The parsed archive operation. */ |
300 | 294 | readonly operation: ArchiverOperation; |
301 | 295 | /** Extra modifier letters attached to the operation token. */ |
302 | | - readonly modifiers: string[]; |
| 296 | + readonly modifiers: readonly string[]; |
303 | 297 | /** Whether thin-archive mode was requested. */ |
304 | 298 | readonly thin: boolean; |
305 | 299 | /** Archive file path when the command syntax provides one. */ |
306 | 300 | readonly archive?: string; |
307 | 301 | /** Member file paths listed after the archive path. */ |
308 | | - readonly members: string[]; |
| 302 | + readonly members: readonly string[]; |
309 | 303 | /** Whether GNU MRI script mode was requested. */ |
310 | 304 | readonly scriptMode: boolean; |
311 | 305 |
|
312 | | - /** |
313 | | - * Creates an archiver analysis from raw argv. |
314 | | - * |
315 | | - * @example |
316 | | - * ```ts |
317 | | - * const analysis = new cmd.ArchiverAnalysis(["ar", "rcs", "liba.a", "a.o"]); |
318 | | - * ``` |
319 | | - */ |
320 | | - constructor(cmd: readonly string[]) { |
321 | | - const resolved = analyzeArchiverModel(cmd); |
322 | | - if (resolved === undefined) { |
323 | | - throw new Error("archiver command analysis required"); |
324 | | - } |
325 | | - |
326 | | - super(resolved.exe, resolved.consume, resolved.produce); |
327 | | - this.operation = resolved.operation; |
328 | | - this.modifiers = [...resolved.modifiers]; |
329 | | - this.thin = resolved.thin; |
330 | | - this.archive = resolved.archive; |
331 | | - this.members = [...resolved.members]; |
332 | | - this.scriptMode = resolved.scriptMode; |
| 306 | + constructor(model: ArchiverModel, command: AnalyzedData) { |
| 307 | + super({ |
| 308 | + exe: command.exe, |
| 309 | + argv: command.argv, |
| 310 | + reads: model.reads, |
| 311 | + writes: model.writes, |
| 312 | + edges: model.writes.map((output) => ({ |
| 313 | + output, |
| 314 | + inputs: [...model.reads], |
| 315 | + })), |
| 316 | + }); |
| 317 | + this.operation = model.operation; |
| 318 | + this.modifiers = [...model.modifiers]; |
| 319 | + this.thin = model.thin; |
| 320 | + this.archive = model.archive; |
| 321 | + this.members = [...model.members]; |
| 322 | + this.scriptMode = model.scriptMode; |
333 | 323 | } |
334 | 324 | } |
335 | 325 |
|
336 | | -const _archiverAnalyzerCheck: Analyzer<ArchiverAnalysis> = ArchiverAnalysis; |
| 326 | +export class ArchiverAnalyzer extends Analyzer { |
| 327 | + readonly kind = "archiver" as const; |
| 328 | + |
| 329 | + analyze( |
| 330 | + command: AnalyzedData, |
| 331 | + ): Result<ArchiverAnalysis, ArchiverAnalysisError> { |
| 332 | + return fromThrowable( |
| 333 | + () => new ArchiverAnalysis(analyzeArchiverModel(command), command), |
| 334 | + (error) => toArchiverAnalysisError(error, "archiver analysis failed"), |
| 335 | + )(); |
| 336 | + } |
| 337 | +} |
0 commit comments