OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Tools for code generation. | |
7 */ | |
8 library codegen.tools; | |
9 | |
10 import 'dart:io'; | |
11 | |
12 import 'package:html/dom.dart' as dom; | |
13 import 'package:path/path.dart'; | |
14 | |
15 import 'html_tools.dart'; | |
16 import 'text_formatter.dart'; | |
17 | |
18 final RegExp trailingWhitespaceRegExp = new RegExp(r'[\n ]+$'); | |
19 final RegExp trailingSpacesInLineRegExp = new RegExp(r' +$', multiLine: true); | |
20 | |
21 /** | |
22 * Join the given strings using camelCase. If [doCapitalize] is true, the first | |
23 * part will be capitalized as well. | |
24 */ | |
25 String camelJoin(List<String> parts, {bool doCapitalize: false}) { | |
26 List<String> upcasedParts = <String>[]; | |
27 for (int i = 0; i < parts.length; i++) { | |
28 if (i == 0 && !doCapitalize) { | |
29 upcasedParts.add(parts[i]); | |
30 } else { | |
31 upcasedParts.add(capitalize(parts[i])); | |
32 } | |
33 } | |
34 return upcasedParts.join(); | |
35 } | |
36 | |
37 /** | |
38 * Capitalize and return the passed String. | |
39 */ | |
40 String capitalize(String string) { | |
41 return string[0].toUpperCase() + string.substring(1); | |
42 } | |
43 | |
44 /** | |
45 * Type of functions used to compute the contents of a set of generated files. | |
46 */ | |
47 typedef Map<String, FileContentsComputer> DirectoryContentsComputer(); | |
48 | |
49 /** | |
50 * Type of functions used to compute the contents of a generated file. | |
51 */ | |
52 typedef String FileContentsComputer(); | |
53 | |
54 /** | |
55 * Mixin class for generating code. | |
56 */ | |
57 class CodeGenerator { | |
58 _CodeGeneratorState _state; | |
59 | |
60 /** | |
61 * Settings that specialize code generation behavior for a given | |
62 * programming language. | |
63 */ | |
64 CodeGeneratorSettings codeGeneratorSettings = new CodeGeneratorSettings(); | |
65 | |
66 /** | |
67 * Measure the width of the current indentation level. | |
68 */ | |
69 int get indentWidth => _state.nextIndent.length; | |
70 | |
71 /** | |
72 * Execute [callback], collecting any code that is output using [write] | |
73 * or [writeln], and return the result as a string. | |
74 */ | |
75 String collectCode(void callback(), {bool removeTrailingNewLine: false}) { | |
76 _CodeGeneratorState oldState = _state; | |
77 try { | |
78 _state = new _CodeGeneratorState(); | |
79 callback(); | |
80 var text = | |
81 _state.buffer.toString().replaceAll(trailingSpacesInLineRegExp, ''); | |
82 if (!removeTrailingNewLine) { | |
83 return text; | |
84 } else { | |
85 return text.replaceAll(trailingWhitespaceRegExp, ''); | |
86 } | |
87 } finally { | |
88 _state = oldState; | |
89 } | |
90 } | |
91 | |
92 /** | |
93 * Generate a doc comment based on the HTML in [docs]. | |
94 * | |
95 * When generating java code, the output is compatible with Javadoc, which | |
96 * understands certain HTML constructs. | |
97 */ | |
98 void docComment(List<dom.Node> docs, {bool removeTrailingNewLine: false}) { | |
99 if (containsOnlyWhitespace(docs)) return; | |
100 writeln(codeGeneratorSettings.docCommentStartMarker); | |
101 int width = codeGeneratorSettings.commentLineLength; | |
102 bool javadocStyle = codeGeneratorSettings.languageName == 'java'; | |
103 indentBy(codeGeneratorSettings.docCommentLineLeader, () { | |
104 write(nodesToText(docs, width - _state.indent.length, javadocStyle, | |
105 removeTrailingNewLine: removeTrailingNewLine)); | |
106 }); | |
107 writeln(codeGeneratorSettings.docCommentEndMarker); | |
108 } | |
109 | |
110 /** | |
111 * Execute [callback], indenting any code it outputs. | |
112 */ | |
113 void indent(void callback()) { | |
114 indentSpecial( | |
115 codeGeneratorSettings.indent, codeGeneratorSettings.indent, callback); | |
116 } | |
117 | |
118 /** | |
119 * Execute [callback], using [additionalIndent] to indent any code it outputs. | |
120 */ | |
121 void indentBy(String additionalIndent, void callback()) => | |
122 indentSpecial(additionalIndent, additionalIndent, callback); | |
123 | |
124 /** | |
125 * Execute [callback], using [additionalIndent] to indent any code it outputs. | |
126 * The first line of output is indented by [firstAdditionalIndent] instead of | |
127 * [additionalIndent]. | |
128 */ | |
129 void indentSpecial( | |
130 String firstAdditionalIndent, String additionalIndent, void callback()) { | |
131 String oldNextIndent = _state.nextIndent; | |
132 String oldIndent = _state.indent; | |
133 try { | |
134 _state.nextIndent += firstAdditionalIndent; | |
135 _state.indent += additionalIndent; | |
136 callback(); | |
137 } finally { | |
138 _state.nextIndent = oldNextIndent; | |
139 _state.indent = oldIndent; | |
140 } | |
141 } | |
142 | |
143 void lineComment(List<dom.Node> docs) { | |
144 if (containsOnlyWhitespace(docs)) { | |
145 return; | |
146 } | |
147 write(codeGeneratorSettings.lineCommentLineLeader); | |
148 int width = codeGeneratorSettings.commentLineLength; | |
149 indentBy(codeGeneratorSettings.lineCommentLineLeader, () { | |
150 write(nodesToText(docs, width - _state.indent.length, false)); | |
151 }); | |
152 } | |
153 | |
154 void outputHeader({bool javaStyle: false}) { | |
155 String header; | |
156 if (codeGeneratorSettings.languageName == 'java') { | |
157 header = ''' | |
158 /* | |
159 * Copyright (c) 2014, the Dart project authors. | |
160 * | |
161 * Licensed under the Eclipse Public License v1.0 (the "License"); you may not u
se this file except | |
162 * in compliance with the License. You may obtain a copy of the License at | |
163 * | |
164 * http://www.eclipse.org/legal/epl-v10.html | |
165 * | |
166 * Unless required by applicable law or agreed to in writing, software distribut
ed under the License | |
167 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY K
IND, either express | |
168 * or implied. See the License for the specific language governing permissions a
nd limitations under | |
169 * the License. | |
170 * | |
171 * This file has been automatically generated. Please do not edit it manually. | |
172 * To regenerate the file, use the script "pkg/analysis_server/tool/spec/generat
e_files". | |
173 */'''; | |
174 } else if (codeGeneratorSettings.languageName == 'python') { | |
175 header = ''' | |
176 # Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
177 # for details. All rights reserved. Use of this source code is governed by a | |
178 # BSD-style license that can be found in the LICENSE file. | |
179 # | |
180 # This file has been automatically generated. Please do not edit it manually. | |
181 # To regenerate the file, use the script | |
182 # "pkg/analysis_server/tool/spec/generate_files". | |
183 '''; | |
184 } else { | |
185 header = ''' | |
186 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
187 // for details. All rights reserved. Use of this source code is governed by a | |
188 // BSD-style license that can be found in the LICENSE file. | |
189 // | |
190 // This file has been automatically generated. Please do not edit it manually. | |
191 // To regenerate the file, use the script | |
192 // "pkg/analysis_server/tool/spec/generate_files". | |
193 '''; | |
194 } | |
195 writeln(header.trim()); | |
196 } | |
197 | |
198 /** | |
199 * Output text without ending the current line. | |
200 */ | |
201 void write(Object obj) { | |
202 _state.write(obj.toString()); | |
203 } | |
204 | |
205 /** | |
206 * Output text, ending the current line. | |
207 */ | |
208 void writeln([Object obj = '']) { | |
209 _state.write('$obj\n'); | |
210 } | |
211 } | |
212 | |
213 /** | |
214 * Controls several settings of [CodeGenerator]. | |
215 * | |
216 * The default settings are valid for generating Java and Dart code. | |
217 */ | |
218 class CodeGeneratorSettings { | |
219 /** | |
220 * Name of the language being generated. Lowercase. | |
221 */ | |
222 String languageName; | |
223 | |
224 /** | |
225 * Marker used in line comments. | |
226 */ | |
227 String lineCommentLineLeader; | |
228 | |
229 /** | |
230 * Start marker for doc comments. | |
231 */ | |
232 String docCommentStartMarker; | |
233 | |
234 /** | |
235 * Line leader for body lines in doc comments. | |
236 */ | |
237 String docCommentLineLeader; | |
238 | |
239 /** | |
240 * End marker for doc comments. | |
241 */ | |
242 String docCommentEndMarker; | |
243 | |
244 /** | |
245 * Line length for doc comment lines. | |
246 */ | |
247 int commentLineLength; | |
248 | |
249 /** | |
250 * String used for indenting code. | |
251 */ | |
252 String indent; | |
253 | |
254 CodeGeneratorSettings( | |
255 {this.languageName: 'java', | |
256 this.lineCommentLineLeader: '// ', | |
257 this.docCommentStartMarker: '/**', | |
258 this.docCommentLineLeader: ' * ', | |
259 this.docCommentEndMarker: ' */', | |
260 this.commentLineLength: 99, | |
261 this.indent: ' '}); | |
262 } | |
263 | |
264 abstract class GeneratedContent { | |
265 FileSystemEntity get outputFile; | |
266 bool check(); | |
267 void generate(); | |
268 } | |
269 | |
270 /** | |
271 * Class representing a single output directory (either generated code or | |
272 * generated HTML). No other content should exist in the directory. | |
273 */ | |
274 class GeneratedDirectory extends GeneratedContent { | |
275 /** | |
276 * The path to the directory that will have the generated content. | |
277 */ | |
278 final String outputDirPath; | |
279 | |
280 /** | |
281 * Callback function that computes the directory contents. | |
282 */ | |
283 final DirectoryContentsComputer directoryContentsComputer; | |
284 | |
285 GeneratedDirectory(this.outputDirPath, this.directoryContentsComputer); | |
286 | |
287 /** | |
288 * Get a Directory object representing the output directory. | |
289 */ | |
290 Directory get outputFile => | |
291 new Directory(joinAll(posix.split(outputDirPath))); | |
292 | |
293 /** | |
294 * Check whether the directory has the correct contents, and return true if it | |
295 * does. | |
296 */ | |
297 @override | |
298 bool check() { | |
299 Map<String, FileContentsComputer> map = directoryContentsComputer(); | |
300 try { | |
301 for (String file in map.keys) { | |
302 FileContentsComputer fileContentsComputer = map[file]; | |
303 String expectedContents = fileContentsComputer(); | |
304 File outputFile = | |
305 new File(joinAll(posix.split(posix.join(outputDirPath, file)))); | |
306 String actualContents = outputFile.readAsStringSync(); | |
307 // Normalize Windows line endings to Unix line endings so that the | |
308 // comparison doesn't fail on Windows. | |
309 actualContents = actualContents.replaceAll('\r\n', '\n'); | |
310 if (expectedContents != actualContents) { | |
311 return false; | |
312 } | |
313 } | |
314 int nonHiddenFileCount = 0; | |
315 outputFile | |
316 .listSync(recursive: false, followLinks: false) | |
317 .forEach((FileSystemEntity fileSystemEntity) { | |
318 if (fileSystemEntity is File && | |
319 !basename(fileSystemEntity.path).startsWith('.')) { | |
320 nonHiddenFileCount++; | |
321 } | |
322 }); | |
323 if (nonHiddenFileCount != map.length) { | |
324 // The number of files generated doesn't match the number we expected to | |
325 // generate. | |
326 return false; | |
327 } | |
328 } catch (e) { | |
329 // There was a problem reading the file (most likely because it didn't | |
330 // exist). Treat that the same as if the file doesn't have the expected | |
331 // contents. | |
332 return false; | |
333 } | |
334 return true; | |
335 } | |
336 | |
337 /** | |
338 * Replace the directory with the correct contents. [spec] is the "tool/spec" | |
339 * directory. If [spec] is unspecified, it is assumed to be the directory | |
340 * containing Platform.executable. | |
341 */ | |
342 @override | |
343 void generate() { | |
344 try { | |
345 // delete the contents of the directory (and the directory itself) | |
346 outputFile.deleteSync(recursive: true); | |
347 } catch (e) { | |
348 // Error caught while trying to delete the directory, this can happen if | |
349 // it didn't yet exist. | |
350 } | |
351 // re-create the empty directory | |
352 outputFile.createSync(recursive: true); | |
353 | |
354 // generate all of the files in the directory | |
355 Map<String, FileContentsComputer> map = directoryContentsComputer(); | |
356 map.forEach((String file, FileContentsComputer fileContentsComputer) { | |
357 File outputFile = new File(joinAll(posix.split(outputDirPath + file))); | |
358 outputFile.writeAsStringSync(fileContentsComputer()); | |
359 }); | |
360 } | |
361 } | |
362 | |
363 /** | |
364 * Class representing a single output file (either generated code or generated | |
365 * HTML). | |
366 */ | |
367 class GeneratedFile extends GeneratedContent { | |
368 /** | |
369 * The output file to which generated output should be written, relative to | |
370 * the "tool/spec" directory. This filename uses the posix path separator | |
371 * ('/') regardless of the OS. | |
372 */ | |
373 final String outputPath; | |
374 | |
375 /** | |
376 * Callback function which computes the file. | |
377 */ | |
378 final FileContentsComputer computeContents; | |
379 | |
380 GeneratedFile(this.outputPath, this.computeContents); | |
381 | |
382 /** | |
383 * Get a File object representing the output file. | |
384 */ | |
385 File get outputFile => new File(joinAll(posix.split(outputPath))); | |
386 | |
387 /** | |
388 * Check whether the file has the correct contents, and return true if it | |
389 * does. | |
390 */ | |
391 @override | |
392 bool check() { | |
393 String expectedContents = computeContents(); | |
394 try { | |
395 String actualContents = outputFile.readAsStringSync(); | |
396 // Normalize Windows line endings to Unix line endings so that the | |
397 // comparison doesn't fail on Windows. | |
398 actualContents = actualContents.replaceAll('\r\n', '\n'); | |
399 return expectedContents == actualContents; | |
400 } catch (e) { | |
401 // There was a problem reading the file (most likely because it didn't | |
402 // exist). Treat that the same as if the file doesn't have the expected | |
403 // contents. | |
404 return false; | |
405 } | |
406 } | |
407 | |
408 /** | |
409 * Replace the file with the correct contents. [spec] is the "tool/spec" | |
410 * directory. If [spec] is unspecified, it is assumed to be the directory | |
411 * containing Platform.executable. | |
412 */ | |
413 void generate() { | |
414 outputFile.writeAsStringSync(computeContents()); | |
415 } | |
416 } | |
417 | |
418 /** | |
419 * Mixin class for generating HTML representations of code that are suitable | |
420 * for enclosing inside a <pre> element. | |
421 */ | |
422 abstract class HtmlCodeGenerator { | |
423 _HtmlCodeGeneratorState _state; | |
424 | |
425 /** | |
426 * Add the given [node] to the HTML output. | |
427 */ | |
428 void add(dom.Node node) { | |
429 _state.add(node); | |
430 } | |
431 | |
432 /** | |
433 * Add the given [nodes] to the HTML output. | |
434 */ | |
435 void addAll(Iterable<dom.Node> nodes) { | |
436 for (dom.Node node in nodes) { | |
437 _state.add(node); | |
438 } | |
439 } | |
440 | |
441 /** | |
442 * Execute [callback], collecting any code that is output using [write], | |
443 * [writeln], [add], or [addAll], and return the result as a list of DOM | |
444 * nodes. | |
445 */ | |
446 List<dom.Node> collectHtml(void callback()) { | |
447 _HtmlCodeGeneratorState oldState = _state; | |
448 try { | |
449 _state = new _HtmlCodeGeneratorState(); | |
450 if (callback != null) { | |
451 callback(); | |
452 } | |
453 return _state.buffer; | |
454 } finally { | |
455 _state = oldState; | |
456 } | |
457 } | |
458 | |
459 /** | |
460 * Execute [callback], wrapping its output in an element with the given | |
461 * [name] and [attributes]. | |
462 */ | |
463 void element(String name, Map<String, String> attributes, [void callback()]) { | |
464 add(makeElement(name, attributes, collectHtml(callback))); | |
465 } | |
466 | |
467 /** | |
468 * Execute [callback], indenting any code it outputs by two spaces. | |
469 */ | |
470 void indent(void callback()) { | |
471 String oldIndent = _state.indent; | |
472 try { | |
473 _state.indent += ' '; | |
474 callback(); | |
475 } finally { | |
476 _state.indent = oldIndent; | |
477 } | |
478 } | |
479 | |
480 /** | |
481 * Output text without ending the current line. | |
482 */ | |
483 void write(Object obj) { | |
484 _state.write(obj.toString()); | |
485 } | |
486 | |
487 /** | |
488 * Output text, ending the current line. | |
489 */ | |
490 void writeln([Object obj = '']) { | |
491 _state.write('$obj\n'); | |
492 } | |
493 } | |
494 | |
495 /** | |
496 * State used by [CodeGenerator]. | |
497 */ | |
498 class _CodeGeneratorState { | |
499 StringBuffer buffer = new StringBuffer(); | |
500 String nextIndent = ''; | |
501 String indent = ''; | |
502 bool indentNeeded = true; | |
503 | |
504 void write(String text) { | |
505 List<String> lines = text.split('\n'); | |
506 for (int i = 0; i < lines.length; i++) { | |
507 if (i == lines.length - 1 && lines[i].isEmpty) { | |
508 break; | |
509 } | |
510 if (indentNeeded) { | |
511 buffer.write(nextIndent); | |
512 nextIndent = indent; | |
513 } | |
514 indentNeeded = false; | |
515 buffer.write(lines[i]); | |
516 if (i != lines.length - 1) { | |
517 buffer.writeln(); | |
518 indentNeeded = true; | |
519 } | |
520 } | |
521 } | |
522 } | |
523 | |
524 /** | |
525 * State used by [HtmlCodeGenerator]. | |
526 */ | |
527 class _HtmlCodeGeneratorState { | |
528 List<dom.Node> buffer = <dom.Node>[]; | |
529 String indent = ''; | |
530 bool indentNeeded = true; | |
531 | |
532 void add(dom.Node node) { | |
533 if (node is dom.Text) { | |
534 write(node.text); | |
535 } else { | |
536 buffer.add(node); | |
537 } | |
538 } | |
539 | |
540 void write(String text) { | |
541 if (text.isEmpty) { | |
542 return; | |
543 } | |
544 if (indentNeeded) { | |
545 buffer.add(new dom.Text(indent)); | |
546 } | |
547 List<String> lines = text.split('\n'); | |
548 if (lines.last.isEmpty) { | |
549 lines.removeLast(); | |
550 buffer.add(new dom.Text(lines.join('\n$indent') + '\n')); | |
551 indentNeeded = true; | |
552 } else { | |
553 buffer.add(new dom.Text(lines.join('\n$indent'))); | |
554 indentNeeded = false; | |
555 } | |
556 } | |
557 } | |
OLD | NEW |