dart-sass/tool/grind/synchronize.dart
2022-09-03 01:41:06 +02:00

248 lines
8.0 KiB
Dart

// Copyright 2017 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:convert';
import 'dart:io';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:crypto/crypto.dart';
import 'package:dart_style/dart_style.dart';
import 'package:grinder/grinder.dart';
import 'package:path/path.dart' as p;
import 'package:sass/src/util/nullable.dart';
/// The files to compile to synchronous versions.
final sources = const {
'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
'lib/src/async_compile.dart': 'lib/src/compile.dart',
'lib/src/async_environment.dart': 'lib/src/environment.dart',
'lib/src/async_import_cache.dart': 'lib/src/import_cache.dart'
};
/// Classes that are defined in the async version of a file and used as-is in
/// the sync version, and thus should not be copied over.
final _sharedClasses = const ['EvaluateResult'];
/// This is how we support both synchronous and asynchronous compilation modes.
///
/// Both modes are necessary. Synchronous mode is faster, works in sync-only
/// contexts, and allows us to support Node Sass's renderSync() method.
/// Asynchronous mode allows users to write async importers and functions in
/// both Dart and JS.
///
/// The logic for synchronous and asynchronous mode is identical, but the async
/// code needs to await every statement and expression evaluation in case they
/// do asynchronous work. To avoid duplicating logic, we hand-write asynchronous
/// code for the evaluator and the environment and use this task to compile it
/// to a synchronous equivalent.
@Task('Compile async code to synchronous code.')
void synchronize() {
sources.forEach((source, target) {
var visitor = _Visitor(File(source).readAsStringSync(), source);
parseFile(path: source, featureSet: FeatureSet.latestLanguageVersion())
.unit
.accept(visitor);
var formatted = DartFormatter().format(visitor.result);
File(target).writeAsStringSync(formatted);
});
}
/// The visitor that traverses the asynchronous parse tree and converts it to
/// synchronous code.
///
/// To preserve the original whitespace and comments, this copies text from the
/// original source where possible. It tracks the [_position] at the end of the
/// text that's been written, and writes from that position to the new position
/// whenever text needs to be emitted.
class _Visitor extends RecursiveAstVisitor<void> {
/// The source of the original asynchronous file.
final String _source;
/// The current position in [_source].
var _position = 0;
/// The buffer in which the text of the synchronous file is built up.
final _buffer = StringBuffer();
/// The synchronous text.
String get result {
_buffer.write(_source.substring(_position));
_position = _source.length;
return _buffer.toString();
}
_Visitor(this._source, String path) {
var afterHeader = "\n".allMatches(_source).skip(3).first.end;
_buffer.writeln(_source.substring(0, afterHeader));
_buffer.writeln("""
// DO NOT EDIT. This file was generated from ${p.basename(path)}.
// See tool/grind/synchronize.dart for details.
//
// Checksum: ${sha1.convert(utf8.encode(_source))}
//
// ignore_for_file: unused_import
""");
if (p.basename(path) == 'async_evaluate.dart') {
_buffer.writeln();
_buffer.writeln("import 'async_evaluate.dart' show EvaluateResult;");
_buffer.writeln("export 'async_evaluate.dart' show EvaluateResult;");
_buffer.writeln();
} else if (p.basename(path) == 'async_compile.dart') {
_buffer.writeln();
_buffer.writeln("export 'async_compile.dart';");
_buffer.writeln();
}
_position = afterHeader;
}
void visitAwaitExpression(AwaitExpression node) {
_skip(node.awaitKeyword);
// Skip the space after "await" to work around dart-lang/dart_style#226.
_position++;
node.expression.accept(this);
}
void visitParenthesizedExpression(ParenthesizedExpression node) {
if (node.expression is AwaitExpression) {
_skip(node.leftParenthesis);
node.expression.accept(this);
_skip(node.rightParenthesis);
} else {
node.expression.accept(this);
}
}
void visitBlockFunctionBody(BlockFunctionBody node) {
_skip(node.keyword);
node.visitChildren(this);
}
void visitClassDeclaration(ClassDeclaration node) {
if (_sharedClasses.contains(node.name2.lexeme)) {
_skipNode(node);
} else {
super.visitClassDeclaration(node);
}
}
void visitExpressionFunctionBody(ExpressionFunctionBody node) {
_skip(node.keyword);
node.visitChildren(this);
}
void visitMethodDeclaration(MethodDeclaration node) {
if (_synchronizeName(node.name2.lexeme) != node.name2.lexeme) {
// If the file defines any asynchronous versions of synchronous functions,
// remove them.
_skipNode(node);
} else {
super.visitMethodDeclaration(node);
}
}
void visitImportDirective(ImportDirective node) {
_skipNode(node);
var text = node.toString();
if (!text.contains("dart:async")) {
_buffer.write(text.replaceAll("async_", ""));
}
}
void visitMethodInvocation(MethodInvocation node) {
// Convert async utility methods to their synchronous equivalents.
if (node.target == null &&
["mapAsync", "putIfAbsentAsync"].contains(node.methodName.name)) {
_writeTo(node);
var arguments = node.argumentList.arguments;
_write(arguments.first);
_buffer.write(".${_synchronizeName(node.methodName.name)}");
node.typeArguments.andThen(_write);
_buffer.write("(");
_position = arguments[1].beginToken.offset;
for (var argument in arguments.skip(1)) {
argument.accept(this);
}
} else {
super.visitMethodInvocation(node);
}
}
void visitSimpleIdentifier(SimpleIdentifier node) {
_skip(node.token);
_buffer.write(_synchronizeName(node.name));
}
void visitNamedType(NamedType node) {
if (["Future", "FutureOr"].contains(node.name.name)) {
_skip(node.name.beginToken);
var typeArguments = node.typeArguments;
if (typeArguments != null) {
_skip(typeArguments.leftBracket);
typeArguments.arguments.first.accept(this);
_skip(typeArguments.rightBracket);
} else {
_buffer.write("void");
}
} else if (node.name.name == "Module") {
_skipNode(node);
_buffer.write("Module<Callable>");
} else {
super.visitNamedType(node);
}
}
/// Writes [_source] to [_buffer] up to the beginning of [token], then puts
/// [_position] after [token] so it doesn't get written.
void _skip(Token? token) {
if (token == null) return;
_buffer.write(_source.substring(_position, token.offset));
_position = token.end;
}
/// Writes [_source] to [_buffer] up to the beginning of [node], then puts
/// [_position] after [node] so it doesn't get written.
void _skipNode(AstNode node) {
_writeTo(node);
_position = node.endToken.end;
}
/// Writes [_source] to [_buffer] up to the beginning of [node].
void _writeTo(AstNode node) {
_buffer.write(_source.substring(_position, node.beginToken.offset));
_position = node.beginToken.offset;
}
/// Writes the contents of [node] to [_buffer].
///
/// This leaves [_position] at the end of [node].
void _write(AstNode node) {
_position = node.beginToken.offset;
node.accept(this);
_buffer.write(_source.substring(_position, node.endToken.end));
_position = node.endToken.end;
}
/// Strips an "async" prefix or suffix from [name].
String _synchronizeName(String name) {
if (name.toLowerCase().startsWith('async')) {
return name.substring('async'.length);
} else if (name.toLowerCase().endsWith('async')) {
return name.substring(0, name.length - 'async'.length);
} else {
return name;
}
}
}