067293b7fc
Change-Id: I8a2e5de067d5ee5a1bd564e5738e990bf289e325 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/99372 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Commit-Queue: Jaime Wren <jwren@google.com>
649 lines
19 KiB
Dart
649 lines
19 KiB
Dart
// 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:analysis_tool/html.dart';
|
|
import 'package:analysis_tool/text_formatter.dart';
|
|
import 'package:html/dom.dart' as dom;
|
|
import 'package:path/path.dart';
|
|
|
|
final RegExp trailingSpacesInLineRegExp = new RegExp(r' +$', multiLine: true);
|
|
final RegExp trailingWhitespaceRegExp = new 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}) {
|
|
List<String> upcasedParts = <String>[];
|
|
for (int 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 Map<String, FileContentsComputer> DirectoryContentsComputer(
|
|
String pkgPath);
|
|
|
|
/**
|
|
* Type of functions used to compute the contents of a generated file.
|
|
* [pkgPath] is the path to the current package.
|
|
*/
|
|
typedef Future<String> FileContentsComputer(String pkgPath);
|
|
|
|
/**
|
|
* Mixin class for generating code.
|
|
*/
|
|
class CodeGenerator {
|
|
_CodeGeneratorState _state;
|
|
|
|
/**
|
|
* Settings that specialize code generation behavior for a given
|
|
* programming language.
|
|
*/
|
|
CodeGeneratorSettings codeGeneratorSettings = new 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 callback(), {bool removeTrailingNewLine: false}) {
|
|
_CodeGeneratorState oldState = _state;
|
|
try {
|
|
_state = new _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;
|
|
if (codeGeneratorSettings.docCommentStartMarker != null)
|
|
writeln(codeGeneratorSettings.docCommentStartMarker);
|
|
int width = codeGeneratorSettings.commentLineLength;
|
|
bool javadocStyle = codeGeneratorSettings.languageName == 'java';
|
|
indentBy(codeGeneratorSettings.docCommentLineLeader, () {
|
|
write(nodesToText(docs, width - _state.indent.length, javadocStyle,
|
|
removeTrailingNewLine: removeTrailingNewLine));
|
|
});
|
|
if (codeGeneratorSettings.docCommentEndMarker != null)
|
|
writeln(codeGeneratorSettings.docCommentEndMarker);
|
|
}
|
|
|
|
/**
|
|
* Execute [callback], indenting any code it outputs.
|
|
*/
|
|
void indent(void callback()) {
|
|
indentSpecial(
|
|
codeGeneratorSettings.indent, codeGeneratorSettings.indent, callback);
|
|
}
|
|
|
|
/**
|
|
* Execute [callback], using [additionalIndent] to indent any code it outputs.
|
|
*/
|
|
void indentBy(String additionalIndent, void 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 callback()) {
|
|
String oldNextIndent = _state.nextIndent;
|
|
String 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);
|
|
int width = codeGeneratorSettings.commentLineLength;
|
|
indentBy(codeGeneratorSettings.lineCommentLineLeader, () {
|
|
write(nodesToText(docs, width - _state.indent.length, false));
|
|
});
|
|
}
|
|
|
|
void outputHeader({bool javaStyle: false, String year = null}) {
|
|
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 dartfmt.
|
|
*/
|
|
class DartFormat {
|
|
static String get _dartfmtPath {
|
|
String binName = Platform.isWindows ? 'dartfmt.bat' : 'dartfmt';
|
|
for (var loc in [binName, join('dart-sdk', 'bin', binName)]) {
|
|
var candidatePath = join(dirname(Platform.resolvedExecutable), loc);
|
|
if (new File(candidatePath).existsSync()) {
|
|
return candidatePath;
|
|
}
|
|
}
|
|
throw new StateError('Could not find dartfmt executable');
|
|
}
|
|
|
|
static void formatFile(File file) {
|
|
ProcessResult result = Process.runSync(_dartfmtPath, ['-w', file.path]);
|
|
if (result.exitCode != 0) throw result.stderr;
|
|
}
|
|
|
|
static String formatText(String text) {
|
|
File file = new File(join(Directory.systemTemp.path, 'gen.dart'));
|
|
file.writeAsStringSync(text);
|
|
ProcessResult result = Process.runSync(_dartfmtPath, ['-w', file.path]);
|
|
if (result.exitCode != 0) throw result.stderr;
|
|
return file.readAsStringSync();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
bool generateNeeded = false;
|
|
for (GeneratedContent target in targets) {
|
|
bool 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:');
|
|
String executable = Platform.executable;
|
|
String packageRoot = '';
|
|
// ignore: deprecated_member_use
|
|
if (Platform.packageRoot != null) {
|
|
// ignore: deprecated_member_use
|
|
packageRoot = ' --package-root=${Platform.packageRoot}';
|
|
}
|
|
String 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 (GeneratedContent 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 {
|
|
Directory outputDirectory = output(pkgPath);
|
|
Map<String, FileContentsComputer> map = directoryContentsComputer(pkgPath);
|
|
try {
|
|
for (String file in map.keys) {
|
|
FileContentsComputer fileContentsComputer = map[file];
|
|
String expectedContents = await fileContentsComputer(pkgPath);
|
|
File outputFile = new File(posix.join(outputDirectory.path, file));
|
|
String 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;
|
|
}
|
|
}
|
|
int 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 {
|
|
Directory 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
|
|
Map<String, FileContentsComputer> map = directoryContentsComputer(pkgPath);
|
|
for (String file in map.keys) {
|
|
FileContentsComputer fileContentsComputer = map[file];
|
|
File outputFile = new File(posix.join(outputDirectory.path, file));
|
|
print(' ${outputFile.path}');
|
|
String contents = await fileContentsComputer(pkgPath);
|
|
outputFile.writeAsStringSync(contents);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Directory output(String pkgPath) =>
|
|
new 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 {
|
|
File outputFile = output(pkgPath);
|
|
String expectedContents = await computeContents(pkgPath);
|
|
if (isDartFile) {
|
|
expectedContents = DartFormat.formatText(expectedContents);
|
|
}
|
|
try {
|
|
String 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 {
|
|
File outputFile = output(pkgPath);
|
|
print(' ${outputFile.path}');
|
|
String contents = await computeContents(pkgPath);
|
|
outputFile.writeAsStringSync(contents);
|
|
if (isDartFile) {
|
|
DartFormat.formatFile(outputFile);
|
|
}
|
|
}
|
|
|
|
@override
|
|
File output(String pkgPath) =>
|
|
new 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;
|
|
|
|
/**
|
|
* 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 (dom.Node 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 callback()) {
|
|
_HtmlCodeGeneratorState oldState = _state;
|
|
try {
|
|
_state = new _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<dynamic, String> attributes,
|
|
[void callback()]) {
|
|
add(makeElement(name, attributes, collectHtml(callback)));
|
|
}
|
|
|
|
/**
|
|
* Execute [callback], indenting any code it outputs by two spaces.
|
|
*/
|
|
void indent(void callback()) {
|
|
String 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 = new StringBuffer();
|
|
String nextIndent = '';
|
|
String indent = '';
|
|
bool indentNeeded = true;
|
|
|
|
void write(String text) {
|
|
List<String> lines = text.split('\n');
|
|
for (int 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(new dom.Text(indent));
|
|
}
|
|
List<String> lines = text.split('\n');
|
|
if (lines.last.isEmpty) {
|
|
lines.removeLast();
|
|
buffer.add(new dom.Text(lines.join('\n$indent') + '\n'));
|
|
indentNeeded = true;
|
|
} else {
|
|
buffer.add(new dom.Text(lines.join('\n$indent')));
|
|
indentNeeded = false;
|
|
}
|
|
}
|
|
}
|