| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| /// Tools for generating code in analyzer and analysis server. |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:analyzer_utilities/html.dart'; |
| import 'package:analyzer_utilities/text_formatter.dart'; |
| import 'package:html/dom.dart' as dom; |
| import 'package:path/path.dart'; |
| |
| final RegExp trailingSpacesInLineRegExp = RegExp(r' +$', multiLine: true); |
| final RegExp trailingWhitespaceRegExp = RegExp(r'[\n ]+$'); |
| |
| /// Join the given strings using camelCase. If [doCapitalize] is true, the first |
| /// part will be capitalized as well. |
| String camelJoin(List<String> parts, {bool doCapitalize = false}) { |
| var upcasedParts = <String>[]; |
| for (var i = 0; i < parts.length; i++) { |
| if (i == 0 && !doCapitalize) { |
| upcasedParts.add(parts[i]); |
| } else { |
| upcasedParts.add(capitalize(parts[i])); |
| } |
| } |
| return upcasedParts.join(); |
| } |
| |
| /// Capitalize and return the passed String. |
| String capitalize(String string) { |
| return string[0].toUpperCase() + string.substring(1); |
| } |
| |
| /// Type of functions used to compute the contents of a set of generated files. |
| /// [pkgPath] is the path to the current package. |
| typedef DirectoryContentsComputer = Map<String, FileContentsComputer> Function( |
| String pkgPath); |
| |
| /// Type of functions used to compute the contents of a generated file. |
| /// [pkgPath] is the path to the current package. |
| typedef FileContentsComputer = Future<String> Function(String pkgPath); |
| |
| /// Mixin class for generating code. |
| class CodeGenerator { |
| _CodeGeneratorState _state = _CodeGeneratorState(); |
| |
| /// Settings that specialize code generation behavior for a given |
| /// programming language. |
| CodeGeneratorSettings codeGeneratorSettings = CodeGeneratorSettings(); |
| |
| /// Measure the width of the current indentation level. |
| int get indentWidth => _state.nextIndent.length; |
| |
| /// Execute [callback], collecting any code that is output using [write] |
| /// or [writeln], and return the result as a string. |
| String collectCode(void Function() callback, |
| {bool removeTrailingNewLine = false}) { |
| var oldState = _state; |
| try { |
| _state = _CodeGeneratorState(); |
| callback(); |
| var text = |
| _state.buffer.toString().replaceAll(trailingSpacesInLineRegExp, ''); |
| if (!removeTrailingNewLine) { |
| return text; |
| } else { |
| return text.replaceAll(trailingWhitespaceRegExp, ''); |
| } |
| } finally { |
| _state = oldState; |
| } |
| } |
| |
| /// Generate a doc comment based on the HTML in [docs]. |
| /// |
| /// When generating java code, the output is compatible with Javadoc, which |
| /// understands certain HTML constructs. |
| void docComment(List<dom.Node> docs, {bool removeTrailingNewLine = false}) { |
| if (containsOnlyWhitespace(docs)) return; |
| |
| var startMarker = codeGeneratorSettings.docCommentStartMarker; |
| if (startMarker != null) { |
| writeln(startMarker); |
| } |
| |
| var width = codeGeneratorSettings.commentLineLength; |
| var javadocStyle = codeGeneratorSettings.languageName == 'java'; |
| indentBy(codeGeneratorSettings.docCommentLineLeader, () { |
| write(nodesToText(docs, width - _state.indent.length, javadocStyle, |
| removeTrailingNewLine: removeTrailingNewLine)); |
| }); |
| |
| var endMarker = codeGeneratorSettings.docCommentEndMarker; |
| if (endMarker != null) { |
| writeln(endMarker); |
| } |
| } |
| |
| /// Execute [callback], indenting any code it outputs. |
| void indent(void Function() callback) { |
| indentSpecial( |
| codeGeneratorSettings.indent, codeGeneratorSettings.indent, callback); |
| } |
| |
| /// Execute [callback], using [additionalIndent] to indent any code it outputs. |
| void indentBy(String additionalIndent, void Function() callback) => |
| indentSpecial(additionalIndent, additionalIndent, callback); |
| |
| /// Execute [callback], using [additionalIndent] to indent any code it outputs. |
| /// The first line of output is indented by [firstAdditionalIndent] instead of |
| /// [additionalIndent]. |
| void indentSpecial(String firstAdditionalIndent, String additionalIndent, |
| void Function() callback) { |
| var oldNextIndent = _state.nextIndent; |
| var oldIndent = _state.indent; |
| try { |
| _state.nextIndent += firstAdditionalIndent; |
| _state.indent += additionalIndent; |
| callback(); |
| } finally { |
| _state.nextIndent = oldNextIndent; |
| _state.indent = oldIndent; |
| } |
| } |
| |
| void lineComment(List<dom.Node> docs) { |
| if (containsOnlyWhitespace(docs)) { |
| return; |
| } |
| write(codeGeneratorSettings.lineCommentLineLeader); |
| var width = codeGeneratorSettings.commentLineLength; |
| indentBy(codeGeneratorSettings.lineCommentLineLeader, () { |
| write(nodesToText(docs, width - _state.indent.length, false)); |
| }); |
| } |
| |
| void outputHeader({bool javaStyle = false, String? year}) { |
| String header; |
| if (codeGeneratorSettings.languageName == 'java') { |
| header = ''' |
| /* |
| * Copyright (c) ${year ?? '2019'}, the Dart project authors. Please see the AUTHORS file |
| * for details. All rights reserved. Use of this source code is governed by a |
| * BSD-style license that can be found in the LICENSE file. |
| * |
| * This file has been automatically generated. Please do not edit it manually. |
| * To regenerate the file, use the script "pkg/analysis_server/tool/spec/generate_files". |
| */'''; |
| } else if (codeGeneratorSettings.languageName == 'python') { |
| header = ''' |
| # Copyright (c) ${year ?? '2014'}, the Dart project authors. Please see the AUTHORS file |
| # for details. All rights reserved. Use of this source code is governed by a |
| # BSD-style license that can be found in the LICENSE file. |
| # |
| # This file has been automatically generated. Please do not edit it manually. |
| # To regenerate the file, use the script |
| # "pkg/analysis_server/tool/spec/generate_files". |
| '''; |
| } else { |
| header = ''' |
| // Copyright (c) ${year ?? '2014'}, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| // |
| // This file has been automatically generated. Please do not edit it manually. |
| // To regenerate the file, use the script |
| // "pkg/analysis_server/tool/spec/generate_files". |
| '''; |
| } |
| writeln(header.trim()); |
| } |
| |
| /// Output text without ending the current line. |
| void write(Object obj) { |
| _state.write(obj.toString()); |
| } |
| |
| /// Output text, ending the current line. |
| void writeln([Object obj = '']) { |
| _state.write('$obj\n'); |
| } |
| } |
| |
| /// Controls several settings of [CodeGenerator]. |
| /// |
| /// The default settings are valid for generating Java and Dart code. |
| class CodeGeneratorSettings { |
| /// Name of the language being generated. Lowercase. |
| String languageName; |
| |
| /// Marker used in line comments. |
| String lineCommentLineLeader; |
| |
| /// Start marker for doc comments. |
| String? docCommentStartMarker; |
| |
| /// Line leader for body lines in doc comments. |
| String docCommentLineLeader; |
| |
| /// End marker for doc comments. |
| String? docCommentEndMarker; |
| |
| /// Line length for doc comment lines. |
| int commentLineLength; |
| |
| /// String used for indenting code. |
| String indent; |
| |
| CodeGeneratorSettings( |
| {this.languageName = 'java', |
| this.lineCommentLineLeader = '// ', |
| this.docCommentStartMarker = '/**', |
| this.docCommentLineLeader = ' * ', |
| this.docCommentEndMarker = ' */', |
| this.commentLineLength = 99, |
| this.indent = ' '}); |
| } |
| |
| /// A utility class for invoking 'dart format'. |
| class DartFormat { |
| static String get _dartPath => Platform.resolvedExecutable; |
| |
| static void formatFile(File file) { |
| var result = Process.runSync(_dartPath, ['format', file.path]); |
| _throwIfExitCode(result); |
| } |
| |
| static String formatText(String text) { |
| var file = File(join(Directory.systemTemp.path, 'gen.dart')); |
| file.writeAsStringSync(text); |
| formatFile(file); |
| return file.readAsStringSync(); |
| } |
| |
| static void _throwIfExitCode(ProcessResult result) { |
| if (result.exitCode != 0) throw result.stderr as Object; |
| } |
| } |
| |
| /// Abstract base class representing behaviors common to generated files and |
| /// generated directories. |
| abstract class GeneratedContent { |
| /// Check whether the [output] has the correct contents, and return true if it |
| /// does. [pkgPath] is the path to the current package. |
| Future<bool> check(String pkgPath); |
| |
| /// Replace the [output] with the correct contents. [pkgPath] is the path to |
| /// the current package. |
| Future<void> generate(String pkgPath); |
| |
| /// Get a [FileSystemEntity] representing the output file or directory. |
| /// [pkgPath] is the path to the current package. |
| FileSystemEntity output(String pkgPath); |
| |
| /// Check that all of the [targets] are up to date. If they are not, print |
| /// out a message instructing the user to regenerate them, and exit with a |
| /// nonzero error code. |
| /// |
| /// [pkgPath] is the path to the current package. [generatorPath] is the path |
| /// to a .dart script the user may use to regenerate the targets. |
| /// |
| /// To avoid mistakes when run on Windows, [generatorPath] always uses |
| /// POSIX directory separators. |
| static Future<void> checkAll( |
| String pkgPath, String generatorPath, Iterable<GeneratedContent> targets, |
| {List<String> args = const []}) async { |
| var generateNeeded = false; |
| for (var target in targets) { |
| var ok = await target.check(pkgPath); |
| if (!ok) { |
| print('${target.output(pkgPath).absolute}' |
| " doesn't have expected contents."); |
| generateNeeded = true; |
| } |
| } |
| if (generateNeeded) { |
| print('Please regenerate using:'); |
| var executable = Platform.executable; |
| var packageRoot = ''; |
| // ignore: deprecated_member_use |
| if (Platform.packageRoot != null) { |
| // ignore: deprecated_member_use |
| packageRoot = ' --package-root=${Platform.packageRoot}'; |
| } |
| var generateScript = normalize(joinAll(posix.split(generatorPath))); |
| print(' $executable$packageRoot $generateScript ${args.join(" ")}'); |
| exit(1); |
| } else { |
| print('All generated files up to date.'); |
| } |
| } |
| |
| /// Regenerate all of the [targets]. [pkgPath] is the path to the current |
| /// package. |
| static Future<void> generateAll( |
| String pkgPath, Iterable<GeneratedContent> targets) async { |
| print('Generating...'); |
| for (var target in targets) { |
| await target.generate(pkgPath); |
| } |
| } |
| } |
| |
| /// Class representing a single output directory (either generated code or |
| /// generated HTML). No other content should exist in the directory. |
| class GeneratedDirectory extends GeneratedContent { |
| /// The path to the directory that will have the generated content. |
| final String outputDirPath; |
| |
| /// Callback function that computes the directory contents. |
| final DirectoryContentsComputer directoryContentsComputer; |
| |
| GeneratedDirectory(this.outputDirPath, this.directoryContentsComputer); |
| |
| @override |
| Future<bool> check(String pkgPath) async { |
| var outputDirectory = output(pkgPath); |
| var map = directoryContentsComputer(pkgPath); |
| try { |
| for (var entry in map.entries) { |
| var file = entry.key; |
| var fileContentsComputer = entry.value; |
| var expectedContents = await fileContentsComputer(pkgPath); |
| var outputFile = File(posix.join(outputDirectory.path, file)); |
| var actualContents = outputFile.readAsStringSync(); |
| // Normalize Windows line endings to Unix line endings so that the |
| // comparison doesn't fail on Windows. |
| actualContents = actualContents.replaceAll('\r\n', '\n'); |
| if (expectedContents != actualContents) { |
| return false; |
| } |
| } |
| var nonHiddenFileCount = 0; |
| outputDirectory |
| .listSync(recursive: false, followLinks: false) |
| .forEach((FileSystemEntity fileSystemEntity) { |
| if (fileSystemEntity is File && |
| !basename(fileSystemEntity.path).startsWith('.')) { |
| nonHiddenFileCount++; |
| } |
| }); |
| if (nonHiddenFileCount != map.length) { |
| // The number of files generated doesn't match the number we expected to |
| // generate. |
| return false; |
| } |
| } catch (e) { |
| // There was a problem reading the file (most likely because it didn't |
| // exist). Treat that the same as if the file doesn't have the expected |
| // contents. |
| return false; |
| } |
| return true; |
| } |
| |
| @override |
| Future<void> generate(String pkgPath) async { |
| var outputDirectory = output(pkgPath); |
| try { |
| // delete the contents of the directory (and the directory itself) |
| outputDirectory.deleteSync(recursive: true); |
| } catch (e) { |
| // Error caught while trying to delete the directory, this can happen if |
| // it didn't yet exist. |
| } |
| // re-create the empty directory |
| outputDirectory.createSync(recursive: true); |
| |
| // generate all of the files in the directory |
| var map = directoryContentsComputer(pkgPath); |
| for (var entry in map.entries) { |
| var file = entry.key; |
| var fileContentsComputer = entry.value; |
| var outputFile = File(posix.join(outputDirectory.path, file)); |
| print(' ${outputFile.path}'); |
| var contents = await fileContentsComputer(pkgPath); |
| outputFile.writeAsStringSync(contents); |
| } |
| } |
| |
| @override |
| Directory output(String pkgPath) => |
| Directory(join(pkgPath, joinAll(posix.split(outputDirPath)))); |
| } |
| |
| /// Class representing a single output file (either generated code or generated |
| /// HTML). |
| class GeneratedFile extends GeneratedContent { |
| /// The output file to which generated output should be written, relative to |
| /// the "tool/spec" directory. This filename uses the posix path separator |
| /// ('/') regardless of the OS. |
| final String outputPath; |
| |
| /// Callback function which computes the file. |
| final FileContentsComputer computeContents; |
| |
| GeneratedFile(this.outputPath, this.computeContents); |
| |
| bool get isDartFile => outputPath.endsWith('.dart'); |
| |
| @override |
| Future<bool> check(String pkgPath) async { |
| var outputFile = output(pkgPath); |
| var expectedContents = await computeContents(pkgPath); |
| if (isDartFile) { |
| expectedContents = DartFormat.formatText(expectedContents); |
| } |
| try { |
| var actualContents = outputFile.readAsStringSync(); |
| // Normalize Windows line endings to Unix line endings so that the |
| // comparison doesn't fail on Windows. |
| actualContents = actualContents.replaceAll('\r\n', '\n'); |
| return expectedContents == actualContents; |
| } catch (e) { |
| // There was a problem reading the file (most likely because it didn't |
| // exist). Treat that the same as if the file doesn't have the expected |
| // contents. |
| return false; |
| } |
| } |
| |
| @override |
| Future<void> generate(String pkgPath) async { |
| var outputFile = output(pkgPath); |
| print(' ${outputFile.path}'); |
| var contents = await computeContents(pkgPath); |
| outputFile.writeAsStringSync(contents); |
| if (isDartFile) { |
| DartFormat.formatFile(outputFile); |
| } |
| } |
| |
| @override |
| File output(String pkgPath) => |
| File(join(pkgPath, joinAll(posix.split(outputPath)))); |
| } |
| |
| /// Mixin class for generating HTML representations of code that are suitable |
| /// for enclosing inside a <pre> element. |
| abstract class HtmlCodeGenerator { |
| _HtmlCodeGeneratorState _state = _HtmlCodeGeneratorState(); |
| |
| /// Add the given [node] to the HTML output. |
| void add(dom.Node node) { |
| _state.add(node); |
| } |
| |
| /// Add the given [nodes] to the HTML output. |
| void addAll(Iterable<dom.Node> nodes) { |
| for (var node in nodes) { |
| _state.add(node); |
| } |
| } |
| |
| /// Execute [callback], collecting any code that is output using [write], |
| /// [writeln], [add], or [addAll], and return the result as a list of DOM |
| /// nodes. |
| List<dom.Node> collectHtml(void Function()? callback) { |
| var oldState = _state; |
| try { |
| _state = _HtmlCodeGeneratorState(); |
| if (callback != null) { |
| callback(); |
| } |
| return _state.buffer; |
| } finally { |
| _state = oldState; |
| } |
| } |
| |
| /// Execute [callback], wrapping its output in an element with the given |
| /// [name] and [attributes]. |
| void element(String name, Map<Object, String> attributes, |
| [void Function()? callback]) { |
| add(makeElement(name, attributes, collectHtml(callback))); |
| } |
| |
| /// Execute [callback], indenting any code it outputs by two spaces. |
| void indent(void Function() callback) { |
| var oldIndent = _state.indent; |
| try { |
| _state.indent += ' '; |
| callback(); |
| } finally { |
| _state.indent = oldIndent; |
| } |
| } |
| |
| /// Output text without ending the current line. |
| void write(Object obj) { |
| _state.write(obj.toString()); |
| } |
| |
| /// Output text, ending the current line. |
| void writeln([Object obj = '']) { |
| _state.write('$obj\n'); |
| } |
| } |
| |
| /// State used by [CodeGenerator]. |
| class _CodeGeneratorState { |
| StringBuffer buffer = StringBuffer(); |
| String nextIndent = ''; |
| String indent = ''; |
| bool indentNeeded = true; |
| |
| void write(String text) { |
| var lines = text.split('\n'); |
| for (var i = 0; i < lines.length; i++) { |
| if (i == lines.length - 1 && lines[i].isEmpty) { |
| break; |
| } |
| if (indentNeeded) { |
| buffer.write(nextIndent); |
| nextIndent = indent; |
| } |
| indentNeeded = false; |
| buffer.write(lines[i]); |
| if (i != lines.length - 1) { |
| buffer.writeln(); |
| indentNeeded = true; |
| } |
| } |
| } |
| } |
| |
| /// State used by [HtmlCodeGenerator]. |
| class _HtmlCodeGeneratorState { |
| List<dom.Node> buffer = <dom.Node>[]; |
| String indent = ''; |
| bool indentNeeded = true; |
| |
| void add(dom.Node node) { |
| if (node is dom.Text) { |
| write(node.text); |
| } else { |
| buffer.add(node); |
| } |
| } |
| |
| void write(String text) { |
| if (text.isEmpty) { |
| return; |
| } |
| if (indentNeeded) { |
| buffer.add(dom.Text(indent)); |
| } |
| var lines = text.split('\n'); |
| if (lines.last.isEmpty) { |
| lines.removeLast(); |
| buffer.add(dom.Text(lines.join('\n$indent') + '\n')); |
| indentNeeded = true; |
| } else { |
| buffer.add(dom.Text(lines.join('\n$indent'))); |
| indentNeeded = false; |
| } |
| } |
| } |