mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-15 02:37:09 +01:00
5b9765c833
This will allow cached imports to be shared outside of the context of a single stylesheet compilation.
238 lines
7.7 KiB
Dart
238 lines
7.7 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:io';
|
|
|
|
import 'package:analyzer/analyzer.dart';
|
|
import 'package:analyzer/dart/ast/token.dart';
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:dart2_constant/convert.dart' as convert;
|
|
import 'package:dart_style/dart_style.dart';
|
|
import 'package:grinder/grinder.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
/// The files to compile to synchronous versions.
|
|
final _sources = {
|
|
'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
|
|
'lib/src/async_environment.dart': 'lib/src/environment.dart',
|
|
'lib/src/async_import_cache.dart': 'lib/src/import_cache.dart'
|
|
};
|
|
|
|
/// A map from source file basenames to imports which should be removed when
|
|
/// generating the output files.
|
|
final _removeImports = {
|
|
'async_import_cache.dart': ['utils.dart']
|
|
};
|
|
|
|
/// 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.')
|
|
synchronize() {
|
|
_sources.forEach((source, target) {
|
|
var visitor = new _Visitor(new File(source).readAsStringSync(), source);
|
|
parseDartFile(source).accept(visitor);
|
|
var formatted = new DartFormatter().format(visitor.result);
|
|
new 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 {
|
|
/// The source of the original asynchronous file.
|
|
final String _source;
|
|
|
|
/// The path to the original asynchronous file.
|
|
final String _sourcePath;
|
|
|
|
/// The current position in [_source].
|
|
var _position = 0;
|
|
|
|
/// The buffer in which the text of the synchronous file is built up.
|
|
final _buffer = new StringBuffer();
|
|
|
|
/// The synchronous text.
|
|
String get result {
|
|
_buffer.write(_source.substring(_position));
|
|
_position = _source.length;
|
|
return _buffer.toString();
|
|
}
|
|
|
|
_Visitor(this._source, this._sourcePath) {
|
|
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(_sourcePath)}.
|
|
// See tool/synchronize.dart for details.
|
|
//
|
|
// Checksum: ${sha1.convert(convert.utf8.encode(_source))}
|
|
""");
|
|
|
|
if (p.basename(_sourcePath) == 'async_evaluate.dart') {
|
|
_buffer.writeln();
|
|
_buffer.writeln("import 'async_evaluate.dart' show EvaluateResult;");
|
|
_buffer.writeln("export 'async_evaluate.dart' show EvaluateResult;");
|
|
_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 (node.name.name == 'EvaluateResult') {
|
|
_skipNode(node);
|
|
} else {
|
|
super.visitClassDeclaration(node);
|
|
}
|
|
}
|
|
|
|
void visitExpressionFunctionBody(ExpressionFunctionBody node) {
|
|
_skip(node.keyword);
|
|
node.visitChildren(this);
|
|
}
|
|
|
|
void visitMethodDeclaration(MethodDeclaration node) {
|
|
if (_synchronizeName(node.name.name) != node.name.name) {
|
|
// 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 url = node.uri.stringValue;
|
|
if (url == "dart:async") return;
|
|
|
|
var removeImports = _removeImports[p.basename(_sourcePath)];
|
|
if (removeImports != null && removeImports.contains(url)) return;
|
|
|
|
_buffer.write(node.toString().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)}");
|
|
if (node.typeArguments != null) _write(node.typeArguments);
|
|
_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 visitTypeName(TypeName node) {
|
|
if (["Future", "FutureOr"].contains(node.name.name)) {
|
|
_skip(node.name.beginToken);
|
|
if (node.typeArguments != null) {
|
|
_skip(node.typeArguments.leftBracket);
|
|
node.typeArguments.arguments.first.accept(this);
|
|
_skip(node.typeArguments.rightBracket);
|
|
} else {
|
|
_buffer.write("void");
|
|
}
|
|
} else {
|
|
super.visitTypeName(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) {
|
|
if (node == null) return;
|
|
_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;
|
|
}
|
|
}
|
|
}
|