ts-morph 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,649 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'mkmf'
6
+ require 'json'
7
+
8
+ # Define Node service code generator first
9
+ def node_service_code
10
+ <<~JAVASCRIPT
11
+ const { Project, SyntaxKind } = require("ts-morph");
12
+ const fs = require("fs");
13
+
14
+ class TsMorphService {
15
+ constructor() {
16
+ this.project = new Project({
17
+ useInMemoryFileSystem: true,
18
+ compilerOptions: {
19
+ jsx: "react",
20
+ target: "ES2020",
21
+ module: "ESNext"
22
+ }
23
+ });
24
+ }
25
+
26
+ transform(code, transformations) {
27
+ const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
28
+
29
+ transformations.forEach(transform => {
30
+ switch (transform.type) {
31
+ case 'addDefaultProps':
32
+ this.addDefaultProps(sourceFile, transform.component, transform.props);
33
+ break;
34
+ case 'wrapComponents':
35
+ this.wrapComponents(sourceFile, transform.target, transform.wrapper);
36
+ break;
37
+ case 'renameComponents':
38
+ this.renameComponents(sourceFile, transform.oldName, transform.newName);
39
+ break;
40
+ case 'addImports':
41
+ this.addImports(sourceFile, transform.imports);
42
+ break;
43
+ case 'injectStyles':
44
+ this.injectStyles(sourceFile, transform.styles);
45
+ break;
46
+ case 'removeComponent':
47
+ this.removeComponent(sourceFile, transform.component);
48
+ break;
49
+ case 'extractComponent':
50
+ this.extractComponent(sourceFile, transform.component);
51
+ break;
52
+ }
53
+ });
54
+
55
+ return sourceFile.getFullText();
56
+ }
57
+
58
+ addDefaultProps(sourceFile, componentName, defaultProps) {
59
+ // Add props to JSX elements
60
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
61
+ const openingElement = element.getOpeningElement();
62
+ if (openingElement.getTagNameNode().getText() === componentName) {
63
+ Object.entries(defaultProps).forEach(([propName, propValue]) => {
64
+ const existingProp = openingElement.getAttribute(propName);
65
+ if (!existingProp) {
66
+ openingElement.addAttribute({
67
+ name: propName,
68
+ initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
69
+ });
70
+ }
71
+ });
72
+ }
73
+ });
74
+
75
+ // Add props to self-closing elements
76
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
77
+ if (element.getTagNameNode().getText() === componentName) {
78
+ Object.entries(defaultProps).forEach(([propName, propValue]) => {
79
+ const existingProp = element.getAttribute(propName);
80
+ if (!existingProp) {
81
+ element.addAttribute({
82
+ name: propName,
83
+ initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
84
+ });
85
+ }
86
+ });
87
+ }
88
+ });
89
+ }
90
+
91
+ wrapComponents(sourceFile, targetComponent, wrapperComponent) {
92
+ const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
93
+ .filter(el => el.getOpeningElement().getTagNameNode().getText() === targetComponent);
94
+
95
+ jsxElements.forEach(element => {
96
+ const elementText = element.getFullText();
97
+ const wrappedCode = `<${wrapperComponent}>${elementText}</${wrapperComponent}>`;
98
+ element.replaceWithText(wrappedCode);
99
+ });
100
+ }
101
+
102
+ renameComponents(sourceFile, oldName, newName) {
103
+ // Rename JSX elements
104
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
105
+ const openingElement = element.getOpeningElement();
106
+ const closingElement = element.getClosingElement();
107
+
108
+ if (openingElement.getTagNameNode().getText() === oldName) {
109
+ openingElement.getTagNameNode().replaceWithText(newName);
110
+ if (closingElement) {
111
+ closingElement.getTagNameNode().replaceWithText(newName);
112
+ }
113
+ }
114
+ });
115
+
116
+ // Rename self-closing elements
117
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
118
+ if (element.getTagNameNode().getText() === oldName) {
119
+ element.getTagNameNode().replaceWithText(newName);
120
+ }
121
+ });
122
+
123
+ // Rename imports
124
+ sourceFile.getImportDeclarations().forEach(importDecl => {
125
+ importDecl.getNamedImports().forEach(namedImport => {
126
+ if (namedImport.getName() === oldName) {
127
+ namedImport.renameAlias(newName);
128
+ }
129
+ });
130
+ });
131
+ }
132
+
133
+ addImports(sourceFile, imports) {
134
+ imports.forEach(imp => {
135
+ const existingImport = sourceFile.getImportDeclaration(
136
+ decl => decl.getModuleSpecifierValue() === imp.from
137
+ );
138
+
139
+ if (existingImport) {
140
+ imp.named?.forEach(name => {
141
+ existingImport.addNamedImport(name);
142
+ });
143
+ } else {
144
+ sourceFile.addImportDeclaration({
145
+ moduleSpecifier: imp.from,
146
+ namedImports: imp.named,
147
+ defaultImport: imp.default
148
+ });
149
+ }
150
+ });
151
+ }
152
+
153
+ injectStyles(sourceFile, styles) {
154
+ Object.entries(styles).forEach(([componentName, styleRules]) => {
155
+ // Handle JSX elements
156
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
157
+ const openingElement = element.getOpeningElement();
158
+ if (openingElement.getTagNameNode().getText() === componentName) {
159
+ const styleAttr = openingElement.getAttribute("style");
160
+ if (styleAttr) {
161
+ const existingValue = styleAttr.getInitializer()?.getText() || "{}";
162
+ const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
163
+ styleAttr.setInitializer(mergedStyles);
164
+ } else {
165
+ openingElement.addAttribute({
166
+ name: "style",
167
+ initializer: `{${JSON.stringify(styleRules)}}`
168
+ });
169
+ }
170
+ }
171
+ });
172
+
173
+ // Handle self-closing elements
174
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
175
+ if (element.getTagNameNode().getText() === componentName) {
176
+ const styleAttr = element.getAttribute("style");
177
+ if (styleAttr) {
178
+ const existingValue = styleAttr.getInitializer()?.getText() || "{}";
179
+ const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
180
+ styleAttr.setInitializer(mergedStyles);
181
+ } else {
182
+ element.addAttribute({
183
+ name: "style",
184
+ initializer: `{${JSON.stringify(styleRules)}}`
185
+ });
186
+ }
187
+ }
188
+ });
189
+ });
190
+ }
191
+
192
+ removeComponent(sourceFile, componentName) {
193
+ // Remove JSX elements
194
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
195
+ if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
196
+ element.remove();
197
+ }
198
+ });
199
+
200
+ // Remove self-closing elements
201
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
202
+ if (element.getTagNameNode().getText() === componentName) {
203
+ element.remove();
204
+ }
205
+ });
206
+ }
207
+
208
+ extractComponent(sourceFile, componentName) {
209
+ const components = [];
210
+
211
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
212
+ if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
213
+ components.push(element.getFullText());
214
+ }
215
+ });
216
+
217
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
218
+ if (element.getTagNameNode().getText() === componentName) {
219
+ components.push(element.getFullText());
220
+ }
221
+ });
222
+
223
+ return components;
224
+ }
225
+
226
+ parse(code) {
227
+ const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
228
+ return this.buildAst(sourceFile);
229
+ }
230
+
231
+ buildAst(node) {
232
+ const ast = {
233
+ kind: SyntaxKind[node.getKind()],
234
+ text: node.getText(),
235
+ children: []
236
+ };
237
+
238
+ node.getChildren().forEach(child => {
239
+ ast.children.push(this.buildAst(child));
240
+ });
241
+
242
+ return ast;
243
+ }
244
+
245
+ format(code) {
246
+ const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
247
+ sourceFile.formatText();
248
+ return sourceFile.getFullText();
249
+ }
250
+ }
251
+
252
+ // Main execution
253
+ if (require.main === module) {
254
+ const args = process.argv.slice(2);
255
+ const operation = args[0];
256
+ const inputFile = args[1];
257
+ const outputFile = args[2];
258
+
259
+ const service = new TsMorphService();
260
+ const code = fs.readFileSync(inputFile, 'utf8');
261
+
262
+ try {
263
+ let result;
264
+
265
+ switch (operation) {
266
+ case 'transform':
267
+ const transformationsFile = args[3];
268
+ const transformations = JSON.parse(fs.readFileSync(transformationsFile, 'utf8'));
269
+ result = service.transform(code, transformations);
270
+ break;
271
+ case 'parse':
272
+ result = JSON.stringify(service.parse(code), null, 2);
273
+ break;
274
+ case 'format':
275
+ result = service.format(code);
276
+ break;
277
+ default:
278
+ throw new Error(`Unknown operation: ${operation}`);
279
+ }
280
+
281
+ fs.writeFileSync(outputFile, result);
282
+ console.log("Success");
283
+ } catch (error) {
284
+ console.error("Error:", error.message);
285
+ process.exit(1);
286
+ }
287
+ }
288
+
289
+ module.exports = TsMorphService;
290
+ JAVASCRIPT
291
+ end
292
+
293
+ # This runs during gem installation
294
+
295
+ def install_node_dependencies
296
+ puts "Installing Node.js dependencies for ts-morph..."
297
+
298
+ # Get the gem root directory
299
+ # Install into the gem's lib/ts/morph/node directory so it's packaged with the gem
300
+ # Write to the same directory used by Ts::Morph::NodeService::NODE_DIR
301
+ node_dir = File.expand_path('../../lib/ts/morph/node', __dir__)
302
+
303
+ # Create node directory
304
+ FileUtils.mkdir_p(node_dir)
305
+
306
+ # Create package.json
307
+ package_json_path = File.join(node_dir, 'package.json')
308
+ unless File.exist?(package_json_path)
309
+ package_content = {
310
+ name: "ts-morph-ruby-service",
311
+ version: "1.0.0",
312
+ private: true,
313
+ dependencies: {
314
+ "ts-morph": "^23.0.0",
315
+ "@babel/parser": "^7.24.0",
316
+ "@babel/traverse": "^7.24.0",
317
+ "@babel/generator": "^7.24.0",
318
+ "@babel/types": "^7.24.0",
319
+ "typescript": "^5.0.0"
320
+ }
321
+ }
322
+
323
+ File.write(package_json_path, JSON.pretty_generate(package_content))
324
+ end
325
+
326
+ # Install npm dependencies
327
+ Dir.chdir(node_dir) do
328
+ unless system("npm install")
329
+ puts "WARNING: Failed to install Node.js dependencies. You may need to run 'npm install' manually in #{node_dir}"
330
+ end
331
+ end
332
+
333
+ # Create the service.js file
334
+ service_script_path = File.join(node_dir, 'service.js')
335
+ unless File.exist?(service_script_path)
336
+ File.write(service_script_path, node_service_code)
337
+ end
338
+
339
+ puts "Node.js dependencies installed successfully!"
340
+ rescue => e
341
+ puts "WARNING: Error installing Node.js dependencies: #{e.message}"
342
+ puts "You may need to install them manually by running: Ts::Morph.install_dependencies"
343
+ end
344
+
345
+ # Check if Node.js and npm are available
346
+ if system("which node > /dev/null 2>&1") && system("which npm > /dev/null 2>&1")
347
+ install_node_dependencies
348
+ else
349
+ puts "WARNING: Node.js and npm are required but not found in PATH"
350
+ puts "Please install Node.js and npm, then run: Ts::Morph.install_dependencies"
351
+ end
352
+
353
+ def node_service_code
354
+ <<~JAVASCRIPT
355
+ const { Project, SyntaxKind } = require("ts-morph");
356
+ const fs = require("fs");
357
+
358
+ class TsMorphService {
359
+ constructor() {
360
+ this.project = new Project({
361
+ useInMemoryFileSystem: true,
362
+ compilerOptions: {
363
+ jsx: "react",
364
+ target: "ES2020",
365
+ module: "ESNext"
366
+ }
367
+ });
368
+ }
369
+
370
+ transform(code, transformations) {
371
+ const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
372
+
373
+ transformations.forEach(transform => {
374
+ switch (transform.type) {
375
+ case 'addDefaultProps':
376
+ this.addDefaultProps(sourceFile, transform.component, transform.props);
377
+ break;
378
+ case 'wrapComponents':
379
+ this.wrapComponents(sourceFile, transform.target, transform.wrapper);
380
+ break;
381
+ case 'renameComponents':
382
+ this.renameComponents(sourceFile, transform.oldName, transform.newName);
383
+ break;
384
+ case 'addImports':
385
+ this.addImports(sourceFile, transform.imports);
386
+ break;
387
+ case 'injectStyles':
388
+ this.injectStyles(sourceFile, transform.styles);
389
+ break;
390
+ case 'removeComponent':
391
+ this.removeComponent(sourceFile, transform.component);
392
+ break;
393
+ case 'extractComponent':
394
+ this.extractComponent(sourceFile, transform.component);
395
+ break;
396
+ }
397
+ });
398
+
399
+ return sourceFile.getFullText();
400
+ }
401
+
402
+ addDefaultProps(sourceFile, componentName, defaultProps) {
403
+ // Add props to JSX elements
404
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
405
+ const openingElement = element.getOpeningElement();
406
+ if (openingElement.getTagNameNode().getText() === componentName) {
407
+ Object.entries(defaultProps).forEach(([propName, propValue]) => {
408
+ const existingProp = openingElement.getAttribute(propName);
409
+ if (!existingProp) {
410
+ openingElement.addAttribute({
411
+ name: propName,
412
+ initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
413
+ });
414
+ }
415
+ });
416
+ }
417
+ });
418
+
419
+ // Add props to self-closing elements
420
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
421
+ if (element.getTagNameNode().getText() === componentName) {
422
+ Object.entries(defaultProps).forEach(([propName, propValue]) => {
423
+ const existingProp = element.getAttribute(propName);
424
+ if (!existingProp) {
425
+ element.addAttribute({
426
+ name: propName,
427
+ initializer: typeof propValue === 'string' ? `"${propValue}"` : `{${JSON.stringify(propValue)}}`
428
+ });
429
+ }
430
+ });
431
+ }
432
+ });
433
+ }
434
+
435
+ wrapComponents(sourceFile, targetComponent, wrapperComponent) {
436
+ const jsxElements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement)
437
+ .filter(el => el.getOpeningElement().getTagNameNode().getText() === targetComponent);
438
+
439
+ jsxElements.forEach(element => {
440
+ const elementText = element.getFullText();
441
+ const wrappedCode = `<${wrapperComponent}>${elementText}</${wrapperComponent}>`;
442
+ element.replaceWithText(wrappedCode);
443
+ });
444
+ }
445
+
446
+ renameComponents(sourceFile, oldName, newName) {
447
+ // Rename JSX elements
448
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
449
+ const openingElement = element.getOpeningElement();
450
+ const closingElement = element.getClosingElement();
451
+
452
+ if (openingElement.getTagNameNode().getText() === oldName) {
453
+ openingElement.getTagNameNode().replaceWithText(newName);
454
+ if (closingElement) {
455
+ closingElement.getTagNameNode().replaceWithText(newName);
456
+ }
457
+ }
458
+ });
459
+
460
+ // Rename self-closing elements
461
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
462
+ if (element.getTagNameNode().getText() === oldName) {
463
+ element.getTagNameNode().replaceWithText(newName);
464
+ }
465
+ });
466
+
467
+ // Rename imports
468
+ sourceFile.getImportDeclarations().forEach(importDecl => {
469
+ importDecl.getNamedImports().forEach(namedImport => {
470
+ if (namedImport.getName() === oldName) {
471
+ namedImport.renameAlias(newName);
472
+ }
473
+ });
474
+ });
475
+ }
476
+
477
+ addImports(sourceFile, imports) {
478
+ imports.forEach(imp => {
479
+ const existingImport = sourceFile.getImportDeclaration(
480
+ decl => decl.getModuleSpecifierValue() === imp.from
481
+ );
482
+
483
+ if (existingImport) {
484
+ imp.named?.forEach(name => {
485
+ existingImport.addNamedImport(name);
486
+ });
487
+ } else {
488
+ sourceFile.addImportDeclaration({
489
+ moduleSpecifier: imp.from,
490
+ namedImports: imp.named,
491
+ defaultImport: imp.default
492
+ });
493
+ }
494
+ });
495
+ }
496
+
497
+ injectStyles(sourceFile, styles) {
498
+ Object.entries(styles).forEach(([componentName, styleRules]) => {
499
+ // Handle JSX elements
500
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
501
+ const openingElement = element.getOpeningElement();
502
+ if (openingElement.getTagNameNode().getText() === componentName) {
503
+ const styleAttr = openingElement.getAttribute("style");
504
+ if (styleAttr) {
505
+ const existingValue = styleAttr.getInitializer()?.getText() || "{}";
506
+ const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
507
+ styleAttr.setInitializer(mergedStyles);
508
+ } else {
509
+ openingElement.addAttribute({
510
+ name: "style",
511
+ initializer: `{${JSON.stringify(styleRules)}}`
512
+ });
513
+ }
514
+ }
515
+ });
516
+
517
+ // Handle self-closing elements
518
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
519
+ if (element.getTagNameNode().getText() === componentName) {
520
+ const styleAttr = element.getAttribute("style");
521
+ if (styleAttr) {
522
+ const existingValue = styleAttr.getInitializer()?.getText() || "{}";
523
+ const mergedStyles = `{...${existingValue.slice(1, -1)}, ...${JSON.stringify(styleRules)}}`;
524
+ styleAttr.setInitializer(mergedStyles);
525
+ } else {
526
+ element.addAttribute({
527
+ name: "style",
528
+ initializer: `{${JSON.stringify(styleRules)}}`
529
+ });
530
+ }
531
+ }
532
+ });
533
+ });
534
+ }
535
+
536
+ removeComponent(sourceFile, componentName) {
537
+ // Remove JSX elements
538
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
539
+ if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
540
+ element.remove();
541
+ }
542
+ });
543
+
544
+ // Remove self-closing elements
545
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
546
+ if (element.getTagNameNode().getText() === componentName) {
547
+ element.remove();
548
+ }
549
+ });
550
+ }
551
+
552
+ extractComponent(sourceFile, componentName) {
553
+ const components = [];
554
+
555
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement).forEach(element => {
556
+ if (element.getOpeningElement().getTagNameNode().getText() === componentName) {
557
+ components.push(element.getFullText());
558
+ }
559
+ });
560
+
561
+ sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).forEach(element => {
562
+ if (element.getTagNameNode().getText() === componentName) {
563
+ components.push(element.getFullText());
564
+ }
565
+ });
566
+
567
+ return components;
568
+ }
569
+
570
+ parse(code) {
571
+ const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
572
+ return this.buildAst(sourceFile);
573
+ }
574
+
575
+ buildAst(node) {
576
+ const ast = {
577
+ kind: SyntaxKind[node.getKind()],
578
+ text: node.getText(),
579
+ children: []
580
+ };
581
+
582
+ node.getChildren().forEach(child => {
583
+ ast.children.push(this.buildAst(child));
584
+ });
585
+
586
+ return ast;
587
+ }
588
+
589
+ format(code) {
590
+ const sourceFile = this.project.createSourceFile("temp.tsx", code, { overwrite: true });
591
+ sourceFile.formatText();
592
+ return sourceFile.getFullText();
593
+ }
594
+ }
595
+
596
+ // Main execution
597
+ if (require.main === module) {
598
+ const args = process.argv.slice(2);
599
+ const operation = args[0];
600
+ const inputFile = args[1];
601
+ const outputFile = args[2];
602
+
603
+ const service = new TsMorphService();
604
+ const code = fs.readFileSync(inputFile, 'utf8');
605
+
606
+ try {
607
+ let result;
608
+
609
+ switch (operation) {
610
+ case 'transform':
611
+ const transformationsFile = args[3];
612
+ const transformations = JSON.parse(fs.readFileSync(transformationsFile, 'utf8'));
613
+ result = service.transform(code, transformations);
614
+ break;
615
+ case 'parse':
616
+ result = JSON.stringify(service.parse(code), null, 2);
617
+ break;
618
+ case 'format':
619
+ result = service.format(code);
620
+ break;
621
+ default:
622
+ throw new Error(`Unknown operation: ${operation}`);
623
+ }
624
+
625
+ fs.writeFileSync(outputFile, result);
626
+ console.log("Success");
627
+ } catch (error) {
628
+ console.error("Error:", error.message);
629
+ process.exit(1);
630
+ }
631
+ }
632
+
633
+ module.exports = TsMorphService;
634
+ JAVASCRIPT
635
+ end
636
+
637
+ # Create a dummy Makefile to satisfy the extension build process
638
+ File.write("Makefile", <<~MAKEFILE)
639
+ # Dummy Makefile for ts-morph gem
640
+ .PHONY: all install clean
641
+
642
+ all:
643
+
644
+ install:
645
+
646
+ clean:
647
+ MAKEFILE
648
+
649
+ exit 0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ts
4
+ module Morph
5
+ # Base error class for ts-morph operations
6
+ class Error < StandardError; end
7
+
8
+ # Raised when transformation fails
9
+ class TransformationError < Error; end
10
+
11
+ # Raised when Node.js service is not available
12
+ class ServiceUnavailableError < Error; end
13
+
14
+ # Raised when parsing fails
15
+ class ParseError < Error; end
16
+ end
17
+ end