mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-30 04:39:03 +01:00
339 lines
11 KiB
Dart
339 lines
11 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 'dart:math' as math;
|
|
|
|
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:collection/collection.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:source_span/source_span.dart';
|
|
|
|
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) =>
|
|
File(target).writeAsStringSync(synchronizeFile(source)));
|
|
}
|
|
|
|
/// Returns the result of synchronizing [source].
|
|
String synchronizeFile(String source) {
|
|
source = p.absolute(source);
|
|
var visitor = _Visitor(File(source).readAsStringSync(), source);
|
|
|
|
parseFile(path: source, featureSet: FeatureSet.latestLanguageVersion())
|
|
.unit
|
|
.accept(visitor);
|
|
return DartFormatter().format(visitor.result);
|
|
}
|
|
|
|
/// 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 path from which [_source] was loaded.
|
|
final String _path;
|
|
|
|
/// The current position in [_source].
|
|
var _position = 0;
|
|
|
|
/// The buffer in which the text of the synchronous file is built up.
|
|
final _buffer = StringBuffer();
|
|
|
|
/// Returns the [SourceFile] which is being rewritten.
|
|
///
|
|
/// This is only used for debugging and error reporting.
|
|
SourceFile get _sourceFile =>
|
|
SourceFile.fromString(_source, url: p.toUri(_path));
|
|
|
|
/// The synchronous text.
|
|
String get result {
|
|
_buffer.write(_source.substring(_position));
|
|
_position = _source.length;
|
|
return _buffer.toString();
|
|
}
|
|
|
|
_Visitor(this._source, this._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.name.lexeme)) {
|
|
_skipNode(node);
|
|
} else {
|
|
for (var child in node.sortedCommentAndAnnotations) {
|
|
child.accept(this);
|
|
}
|
|
_rename(node.name);
|
|
node.typeParameters?.accept(this);
|
|
node.extendsClause?.accept(this);
|
|
node.withClause?.accept(this);
|
|
node.implementsClause?.accept(this);
|
|
node.nativeClause?.accept(this);
|
|
node.members.accept(this);
|
|
}
|
|
}
|
|
|
|
void visitGenericTypeAlias(GenericTypeAlias node) {
|
|
if (_sharedClasses.contains(node.name.lexeme)) {
|
|
_skipNode(node);
|
|
} else {
|
|
for (var child in node.sortedCommentAndAnnotations) {
|
|
child.accept(this);
|
|
}
|
|
_rename(node.name);
|
|
node.typeParameters?.accept(this);
|
|
node.type.accept(this);
|
|
}
|
|
}
|
|
|
|
void visitExpressionFunctionBody(ExpressionFunctionBody node) {
|
|
_skip(node.keyword);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
for (var child in node.sortedCommentAndAnnotations) {
|
|
child.accept(this);
|
|
}
|
|
node.returnType?.accept(this);
|
|
_rename(node.name);
|
|
node.functionExpression.accept(this);
|
|
}
|
|
|
|
void visitMethodDeclaration(MethodDeclaration node) {
|
|
if (_synchronizeName(node.name.lexeme) != node.name.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
|
|
case MethodInvocation(
|
|
target: null,
|
|
methodName: SimpleIdentifier(name: "mapAsync" || "putIfAbsentAsync")
|
|
)) {
|
|
_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 (node.name2.lexeme case "Future" || "FutureOr") {
|
|
_skip(node.name2);
|
|
if (node.typeArguments case var typeArguments?) {
|
|
_skip(typeArguments.leftBracket);
|
|
typeArguments.arguments.first.accept(this);
|
|
_skip(typeArguments.rightBracket);
|
|
} else {
|
|
_buffer.write("void");
|
|
}
|
|
} else if (node.name2.lexeme == "Module") {
|
|
_skipNode(node);
|
|
_buffer.write("Module<Callable>");
|
|
} else {
|
|
super.visitNamedType(node);
|
|
}
|
|
}
|
|
|
|
/// Writes through [node]'s (synchronized) name.
|
|
///
|
|
/// Assumes [node] has a name field with type [Token].
|
|
void _rename(Token token) {
|
|
_skip(token);
|
|
_buffer.write(_synchronizeName(token.lexeme));
|
|
}
|
|
|
|
/// 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;
|
|
if (token.offset < _position) {
|
|
throw _alreadyEmittedException(_spanForToken(token));
|
|
}
|
|
|
|
_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) {
|
|
if (node.beginToken.offset < _position) {
|
|
throw _alreadyEmittedException(_spanForNode(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) {
|
|
if (node.beginToken.offset < _position) {
|
|
throw _alreadyEmittedException(_spanForNode(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;
|
|
}
|
|
}
|
|
|
|
SourceSpanException _alreadyEmittedException(SourceSpan span) {
|
|
var lines = _buffer.toString().split("\n");
|
|
return SourceSpanException(
|
|
"Node was already emitted. Last 3 lines:\n\n" +
|
|
lines
|
|
.slice(math.max(lines.length - 3, 0))
|
|
.map((line) => " $line")
|
|
.join("\n") +
|
|
"\n",
|
|
span);
|
|
}
|
|
|
|
/// Returns a [FileSpan] that represents [token]'s position in the source
|
|
/// file.
|
|
SourceSpan _spanForToken(Token token) =>
|
|
_sourceFile.span(token.offset, token.end);
|
|
|
|
/// Returns a [FileSpan] that represents [token]'s position in the source
|
|
/// file.
|
|
SourceSpan _spanForNode(AstNode node) =>
|
|
_sourceFile.span(node.beginToken.offset, node.endToken.end);
|
|
}
|