// Copyright (c) 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.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/error/codes.dart';

/// Finds invalid, or misplaced language override comments.
class LanguageVersionOverrideVerifier {
  static final _overrideCommentLine = RegExp(r'^\s*//\s*@dart\s*=\s*\d+\.\d+');

  final DiagnosticReporter _diagnosticReporter;

  LanguageVersionOverrideVerifier(this._diagnosticReporter);

  void verify(CompilationUnit unit) {
    _verifyMisplaced(unit);

    Token beginToken = unit.beginToken;
    if (beginToken.type == TokenType.SCRIPT_TAG) {
      beginToken = beginToken.next!;
    }
    Token? commentToken = beginToken.precedingComments;
    while (commentToken != null) {
      if (_findLanguageVersionOverrideComment(commentToken)) {
        // A valid language version override was found. Do not search for any
        // later invalid language version comments.
        return;
      }
      commentToken = commentToken.next;
    }
  }

  /// Look for comments which look almost like a Dart language version override,
  /// according to the spec [1].
  ///
  /// The idea of a comment which looks "almost" like a language version
  /// override is a tricky dance. It is important that this function _not_
  /// falsely report comment lines as an "invalid language version override"
  /// when the user was likely not trying to override the language version. Here
  /// is the general algorithm for deciding what to report:
  ///
  /// * When a comment begins with "@dart" or "dart" (letters in any case),
  ///   followed by optional whitespace, followed by optional non-alphanumeric,
  ///   non-whitespace characters, followed by optional whitespace, followed by
  ///   an optional alphabetical character, followed by a digit, followed by the
  ///   end of the line or a non-alphabetical character, then the comment is
  ///   considered to be an attempt at a language version override comment
  ///   (with one exception, below).
  ///   * If the "@" character is missing before "dart", _and_ the
  ///     non-alphabetical characters are not present, the comment is too
  ///     different from a valid language version override comment, and is not
  ///     considered to be an attempt. Examples include: "/// dart2 is great."
  ///   * If the comment began with more than two slashes, report the comment.
  ///     For example: "/// dart = 2".
  ///   * If the "@" character is missing before "dart", report the comment.
  ///     Examples include: "// dart = 2.0", "// dart @ 2.0".
  ///   * If the letters, "dart", are not all lower case, report the comment.
  ///     For example: "// @Dart = 2".
  ///   * If the non-alphabetical characters are not present or are not the
  ///     single character "=", report the comment. Examples include:
  ///     "// @dart: 2", "// @dart > 2.0", "// @dart >= 2.0", "// @dart 2.0".
  ///   * If the optional alphabetical letter is present, report the comment.
  ///     For example: "// @dart = v2".
  ///   * If the digit is not immediately followed by a "." character, then
  ///     another digit, then optional whitespace, then the end of the line,
  ///     report the comment. Examples include: "// @dart = 2",
  ///     "// @dart = 2,0", "// @dart = 2.15", "// @dart = 2.2.2",
  ///     "// @dart = 2.2 or so".
  ///   * Otherwise, the comment is a valid language version override comment.
  /// * Otherwise, the comment is not considered to be an attempt at a language
  ///   version override comment. Nothing is reported. Examples include:
  ///   "/// dart", "/// dart is great", "// dartisans are great",
  ///   "// dart = java, basically".
  ///
  /// [1] https://github.com/dart-lang/language/blob/master/accepted/future-releases/language-versioning/feature-specification.md#individual-library-language-version-override
  bool _findLanguageVersionOverrideComment(Token commentToken) {
    String comment = commentToken.lexeme;
    int offset = commentToken.offset;
    int length = comment.length;
    int index = 0;

    // TODO(srawlins): Actual whitespace.
    bool isWhitespace(int character) => character == 0x09 || character == 0x20;

    bool isNumeric(int character) => character >= 0x30 && character <= 0x39;

    bool isAlphabetical(int character) =>
        (character >= 0x41 && character <= 0x5A) ||
        (character >= 0x61 && character <= 0x7A);

    void skipWhitespaces() {
      while (index < length && isWhitespace(comment.codeUnitAt(index))) {
        index++;
      }
    }

    // Count the number of `/` characters at the beginning.
    while (index < length && comment.codeUnitAt(index) == 0x2F) {
      index++;
    }
    int slashCount = index;

    skipWhitespaces();
    if (index == length) {
      // This is not an attempted language version override comment.
      return false;
    }

    bool atSignPresent = comment.codeUnitAt(index) == 0x40;
    if (atSignPresent) {
      index++;
    }
    if (length - index < 4) {
      // This is not an attempted language version override comment.
      return false;
    }

    String possibleDart = comment.substring(index, index + 4);
    if (possibleDart.toLowerCase() != 'dart') {
      // This is not an attempted language version override comment.
      return false;
    }

    index += 4;
    skipWhitespaces();
    if (index == length) {
      // This is not an attempted language version override comment.
      return false;
    }

    // The separator between "@dart" and the version number.
    int dartVersionSeparatorStartIndex = index;
    // Move through any other consecutive punctuation, whitespace,
    while (index < length) {
      int possibleSeparatorCharacter = comment.codeUnitAt(index);
      if (isNumeric(possibleSeparatorCharacter) ||
          isAlphabetical(possibleSeparatorCharacter) ||
          isWhitespace(possibleSeparatorCharacter)) {
        break;
      }
      index++;
    }
    if (index == length) {
      // This is not an attempted language version override comment.
      return false;
    }

    int dartVersionSeparatorLength = index - dartVersionSeparatorStartIndex;
    skipWhitespaces();
    if (index == length) {
      // This is not an attempted language version override comment.
      return false;
    }

    bool containsInvalidVersionNumberPrefix = false;
    if (isAlphabetical(comment.codeUnitAt(index))) {
      containsInvalidVersionNumberPrefix = true;
      index++;
      if (index == length) {
        // This is not an attempted language version override comment.
        return false;
      }
    }

    if (!isNumeric(comment.codeUnitAt(index))) {
      // This is not an attempted language version override comment.
      return false;
    }

    if (index + 1 < length && isAlphabetical(comment.codeUnitAt(index + 1))) {
      // This is not an attempted language version override comment.
      return false;
    }

    if (!atSignPresent && dartVersionSeparatorLength == 0) {
      // The comment is too different from a valid language version override
      // comment, like "/// dart2 is great".
      return false;
    }

    // At this point, the comment is considered an "attempted" language version
    // override comment. Check for all issues which would make it an invalid
    // language version override comment.

    if (slashCount > 2) {
      _diagnosticReporter.atOffset(
        offset: offset,
        length: length,
        diagnosticCode: WarningCode.invalidLanguageVersionOverrideTwoSlashes,
      );
      return false;
    }

    if (!atSignPresent) {
      _diagnosticReporter.atOffset(
        offset: offset,
        length: length,
        diagnosticCode: WarningCode.invalidLanguageVersionOverrideAtSign,
      );
      return false;
    }

    if (possibleDart != 'dart') {
      // The 4 characters after `@` are "dart", but in the wrong case.
      _diagnosticReporter.atOffset(
        offset: offset,
        length: length,
        diagnosticCode: WarningCode.invalidLanguageVersionOverrideLowerCase,
      );
      return false;
    }

    if (dartVersionSeparatorLength != 1 ||
        comment.codeUnitAt(dartVersionSeparatorStartIndex) != 0x3D) {
      // The separator between "@dart" and the version number is either not
      // present, or is not a single "=" character.
      _diagnosticReporter.atOffset(
        offset: offset,
        length: length,
        diagnosticCode: WarningCode.invalidLanguageVersionOverrideEquals,
      );
      return false;
    }

    if (containsInvalidVersionNumberPrefix) {
      _diagnosticReporter.atOffset(
        offset: offset,
        length: length,
        diagnosticCode: WarningCode.invalidLanguageVersionOverridePrefix,
      );
      return false;
    }

    void reportInvalidNumber() {
      _diagnosticReporter.atOffset(
        offset: offset,
        length: length,
        diagnosticCode: WarningCode.invalidLanguageVersionOverrideNumber,
      );
    }

    // Nothing preceding the version number makes this comment invalid. Check
    // the format of the version number, and trailing characters.

    // Skip major version.
    while (index < length && isNumeric(comment.codeUnitAt(index))) {
      index++;
    }

    // Skip '.' separator.
    if (index == length || comment.codeUnitAt(index) != 0x2E) {
      reportInvalidNumber();
      return false;
    }
    index++;

    // Skip minor version.
    while (index < length && isNumeric(comment.codeUnitAt(index))) {
      index++;
    }

    skipWhitespaces();

    // OK, no trailing characters.
    if (index == length) {
      return true;
    }

    // This comment is a valid language version override, except for trailing
    // characters.
    _diagnosticReporter.atOffset(
      offset: offset,
      length: length,
      diagnosticCode:
          WarningCode.invalidLanguageVersionOverrideTrailingCharacters,
    );
    return false;
  }

  /// Verify that all language version overrides are before declarations.
  void _verifyMisplaced(CompilationUnit unit) {
    Token firstMeaningfulToken;
    if (unit.directives.isNotEmpty) {
      firstMeaningfulToken = unit.directives.first.beginToken;
    } else if (unit.declarations.isNotEmpty) {
      firstMeaningfulToken = unit.declarations.first.beginToken;
    } else {
      return;
    }

    var token = firstMeaningfulToken.next;
    while (token != null) {
      if (token.offset > firstMeaningfulToken.offset) {
        Token? commentToken = token.precedingComments;
        for (; commentToken != null; commentToken = commentToken.next) {
          var lexeme = commentToken.lexeme;

          var match = _overrideCommentLine.firstMatch(lexeme);
          if (match != null) {
            var atDartStart = lexeme.indexOf('@dart');
            _diagnosticReporter.atOffset(
              offset: commentToken.offset + atDartStart,
              length: match.end - atDartStart,
              diagnosticCode:
                  WarningCode.invalidLanguageVersionOverrideLocation,
            );
          }
        }
      }

      if (token.next == token) {
        break;
      } else {
        token = token.next;
      }
    }
  }
}
