content: // 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:grinder/grinder.dart';
  import 'package:path/path.dart' as p;
  import 'package:source_span/source_span.dart';
  
  import 'package:sass/src/util/map.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() {
    for (var (source, target) in sources.pairs) {
      File(target).writeAsStringSync(synchronizeFile(source));
      DartFmt.format(target);
    }
  }
  
  /// 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 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 if (node.typeArguments == null) {
        _skip(node.name2);
        _buffer.write(_synchronizeName(node.name2.lexeme));
      } 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);
  }
  