| 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 |