diff --git a/.github/workflows/dart_ci.yml b/.github/workflows/dart_ci.yml index 9bf177319..b02b8dfc5 100644 --- a/.github/workflows/dart_ci.yml +++ b/.github/workflows/dart_ci.yml @@ -79,6 +79,9 @@ jobs: analyzer_plugin: runs-on: ubuntu-latest + defaults: + run: + working-directory: ./tools/analyzer_plugin strategy: fail-fast: false matrix: @@ -92,13 +95,23 @@ jobs: - name: Print Dart SDK version run: dart --version + - id: link + name: Override over_react dependency with local path + run: cd ../.. && pub get && dart tool/travis_link_plugin_deps.dart + - id: install name: Install dependencies run: pub get + if: always() && steps.link.outcome == 'success' - - name: Run analyzer_plugin checks - run: | - dart tool/travis_link_plugin_deps.dart - cd ./tools/analyzer_plugin - ./tool/travis.sh + - name: Validate dependencies + run: pub run dependency_validator --no-fatal-pins -i analyzer,build_runner,build_web_compilers,built_value_generator + if: always() && steps.install.outcome == 'success' + + - name: Verify formatting + run: pub run dart_dev format --check + if: always() && matrix.sdk == '2.7.2' && steps.install.outcome == 'success' + + - name: Run tests + run: pub run dart_dev test if: always() && steps.install.outcome == 'success' diff --git a/tools/analyzer_plugin/lib/src/assist/contributor_base.dart b/tools/analyzer_plugin/lib/src/assist/contributor_base.dart index 0115e610e..221edbcf6 100644 --- a/tools/analyzer_plugin/lib/src/assist/contributor_base.dart +++ b/tools/analyzer_plugin/lib/src/assist/contributor_base.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/analyzer.dart'; // ignore: deprecated_member_use import 'package:analyzer/dart/analysis/session.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer_plugin/utilities/assist/assist.dart'; import 'package:meta/meta.dart'; +import 'package:over_react_analyzer_plugin/src/util/analyzer_util.dart'; import 'package:over_react_analyzer_plugin/src/async_plugin_apis/assist.dart'; export 'package:over_react_analyzer_plugin/src/doc_utils/docs_meta_annotation.dart'; diff --git a/tools/analyzer_plugin/lib/src/async_plugin_apis/diagnostic.dart b/tools/analyzer_plugin/lib/src/async_plugin_apis/diagnostic.dart index ebed93aaf..97b2d7518 100644 --- a/tools/analyzer_plugin/lib/src/async_plugin_apis/diagnostic.dart +++ b/tools/analyzer_plugin/lib/src/async_plugin_apis/diagnostic.dart @@ -181,7 +181,7 @@ class _DiagnosticGenerator { // but it doesn't do it for plugin errors, so we need to do that here. final lineInfo = unitResult.unit.lineInfo; final filteredErrors = - filterIgnores(collector.errors, lineInfo, () => IgnoreInfo.calculateIgnores(unitResult.content, lineInfo)); + filterIgnores(collector.errors, lineInfo, () => IgnoreInfo.forDart(unitResult.unit, unitResult.content)); return _GeneratorResult(filteredErrors, notifications); } diff --git a/tools/analyzer_plugin/lib/src/diagnostic/bad_key.dart b/tools/analyzer_plugin/lib/src/diagnostic/bad_key.dart index 7a6a2e303..2eec623c6 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/bad_key.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/bad_key.dart @@ -158,19 +158,19 @@ class BadKeyDiagnostic extends ComponentUsageDiagnosticContributor { collector.addError( dynamicOrObjectCode, result.locationFor(expression), - errorMessageArgs: [type.getDisplayString(), getTypeContextString()], + errorMessageArgs: [type.getDisplayString(withNullability: false), getTypeContextString()], ); } else if (type.isDartCoreBool || type.isDartCoreNull) { collector.addError( lowQualityCode, result.locationFor(expression), - errorMessageArgs: [type.getDisplayString(), getTypeContextString()], + errorMessageArgs: [type.getDisplayString(withNullability: false), getTypeContextString()], ); } else if (inheritsToStringImplFromObject(type?.element)) { collector.addError( toStringCode, result.locationFor(expression), - errorMessageArgs: [type.getDisplayString(), getTypeContextString()], + errorMessageArgs: [type.getDisplayString(withNullability: false), getTypeContextString()], ); } } diff --git a/tools/analyzer_plugin/lib/src/diagnostic/invalid_child.dart b/tools/analyzer_plugin/lib/src/diagnostic/invalid_child.dart index 834ad2cf0..c3e239e92 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/invalid_child.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/invalid_child.dart @@ -72,7 +72,7 @@ class InvalidChildDiagnostic extends ComponentUsageDiagnosticContributor { await collector.addErrorWithFix( code, location, - errorMessageArgs: [invalidType.getDisplayString(), missingBuilderMessageSuffix], + errorMessageArgs: [invalidType.getDisplayString(withNullability: false), missingBuilderMessageSuffix], fixKind: addBuilderInvocationFix, computeFix: () => buildFileEdit(result, (builder) { buildMissingInvocationEdits(argument, builder); @@ -81,7 +81,7 @@ class InvalidChildDiagnostic extends ComponentUsageDiagnosticContributor { } else if (invalidType is FunctionType || invalidType.isDartCoreFunction) { // Functions can be used as children } else { - collector.addError(code, location, errorMessageArgs: [invalidType.getDisplayString()]); + collector.addError(code, location, errorMessageArgs: [invalidType.getDisplayString(withNullability: false)]); } }); } diff --git a/tools/analyzer_plugin/lib/src/diagnostic/missing_cascade_parens.dart b/tools/analyzer_plugin/lib/src/diagnostic/missing_cascade_parens.dart index 2f7c9f870..543bc5735 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/missing_cascade_parens.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/missing_cascade_parens.dart @@ -1,6 +1,6 @@ // ignore: deprecated_member_use -import 'package:analyzer/analyzer.dart' show NodeLocator; import 'package:analyzer/dart/ast/ast.dart'; +import 'package:over_react_analyzer_plugin/src/util/analyzer_util.dart'; import 'package:over_react_analyzer_plugin/src/util/react_types.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:over_react_analyzer_plugin/src/diagnostic/analyzer_debug_helper.dart'; @@ -110,7 +110,7 @@ class MissingCascadeParensDiagnostic extends DiagnosticContributor { continue; } - debug.log('${invocation.function.staticType?.getDisplayString()}'); + debug.log('${invocation.function.staticType?.getDisplayString(withNullability: false)}'); if (isBadFunction && (invocation.function.staticType?.isReactElement ?? false)) { final expr = invocation.function?.tryCast() ?? diff --git a/tools/analyzer_plugin/lib/src/diagnostic/render_return_value.dart b/tools/analyzer_plugin/lib/src/diagnostic/render_return_value.dart index 6f9624d2d..370e7045b 100644 --- a/tools/analyzer_plugin/lib/src/diagnostic/render_return_value.dart +++ b/tools/analyzer_plugin/lib/src/diagnostic/render_return_value.dart @@ -131,14 +131,14 @@ class RenderReturnValueDiagnostic extends DiagnosticContributor { await collector.addErrorWithFix( code, location, - errorMessageArgs: [returnType.getDisplayString(), missingBuilderMessageSuffix], + errorMessageArgs: [returnType.getDisplayString(withNullability: false), missingBuilderMessageSuffix], fixKind: addBuilderInvocationFix, computeFix: () => buildFileEdit(result, (builder) { buildMissingInvocationEdits(returnExpression, builder); }), ); } else { - collector.addError(code, location, errorMessageArgs: [returnType.getDisplayString()]); + collector.addError(code, location, errorMessageArgs: [returnType.getDisplayString(withNullability: false)]); } }); diff --git a/tools/analyzer_plugin/lib/src/util/analyzer_util.dart b/tools/analyzer_plugin/lib/src/util/analyzer_util.dart new file mode 100644 index 000000000..6d6663ed5 --- /dev/null +++ b/tools/analyzer_plugin/lib/src/util/analyzer_util.dart @@ -0,0 +1,584 @@ +// From analyzer 0.39.17 src/dart/ast/utilities.dart +// Permalink: https://github.com/dart-lang/sdk/blob/0412862a9f9dad6af60aa93fa54516d88a993abb/pkg/analyzer/lib/src/dart/ast/utilities.dart +// +// Copyright 2013, the Dart project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import 'dart:collection'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +/// An object used to locate the [AstNode] associated with a source range, given +/// the AST structure built from the source. More specifically, they will return +/// the [AstNode] with the shortest length whose source range completely +/// encompasses the specified range. +class NodeLocator extends UnifyingAstVisitor { + /// The start offset of the range used to identify the node. + final int _startOffset; + + /// The end offset of the range used to identify the node. + final int _endOffset; + + /// The element that was found that corresponds to the given source range, or + /// `null` if there is no such element. + AstNode _foundNode; + + /// Initialize a newly created locator to locate an [AstNode] by locating the + /// node within an AST structure that corresponds to the given range of + /// characters (between the [startOffset] and [endOffset] in the source. + NodeLocator(int startOffset, [int endOffset]) + : _startOffset = startOffset, + _endOffset = endOffset ?? startOffset; + + /// Return the node that was found that corresponds to the given source range + /// or `null` if there is no such node. + AstNode get foundNode => _foundNode; + + /// Search within the given AST [node] for an identifier representing an + /// element in the specified source range. Return the element that was found, + /// or `null` if no element was found. + AstNode searchWithin(AstNode node) { + if (node == null) { + return null; + } + try { + node.accept(this); + } catch (_) { + return null; + } + return _foundNode; + } + + @override + void visitNode(AstNode node) { + // Don't visit a new tree if the result has been already found. + if (_foundNode != null) { + return; + } + // Check whether the current node covers the selection. + var beginToken = node.beginToken; + var endToken = node.endToken; + // Don't include synthetic tokens. + while (endToken != beginToken) { + // Fasta scanner reports unterminated string literal errors + // and generates a synthetic string token with non-zero length. + // Because of this, check for length > 0 rather than !isSynthetic. + if (endToken.type == TokenType.EOF || endToken.length > 0) { + break; + } + endToken = endToken.previous; + } + var end = endToken.end; + var start = node.offset; + if (end < _startOffset || start > _endOffset) { + return; + } + // Check children. + try { + node.visitChildren(this); + } catch (_) { + // Ignore the exception and proceed in order to visit the rest of the + // structure. + } + // Found a child. + if (_foundNode != null) { + return; + } + // Check this node. + if (start <= _startOffset && _endOffset <= end) { + _foundNode = node; + } + } +} + +/// An object used to locate the [AstNode] associated with a source range. +/// More specifically, they will return the deepest [AstNode] which completely +/// encompasses the specified range. +class NodeLocator2 extends UnifyingAstVisitor { + /// The inclusive start offset of the range used to identify the node. + final int _startOffset; + + /// The inclusive end offset of the range used to identify the node. + final int _endOffset; + + /// The found node or `null` if there is no such node. + AstNode _foundNode; + + /// Initialize a newly created locator to locate the deepest [AstNode] for + /// which `node.offset <= [startOffset]` and `[endOffset] < node.end`. + /// + /// If [endOffset] is not provided, then it is considered the same as the + /// given [startOffset]. + NodeLocator2(int startOffset, [int endOffset]) + : _startOffset = startOffset, + _endOffset = endOffset ?? startOffset; + + /// Search within the given AST [node] and return the node that was found, + /// or `null` if no node was found. + AstNode searchWithin(AstNode node) { + if (node == null) { + return null; + } + try { + node.accept(this); + } catch (_) { + return null; + } + return _foundNode; + } + + @override + void visitNode(AstNode node) { + // Don't visit a new tree if the result has been already found. + if (_foundNode != null) { + return; + } + // Check whether the current node covers the selection. + var beginToken = node.beginToken; + var endToken = node.endToken; + // Don't include synthetic tokens. + while (endToken != beginToken) { + // Fasta scanner reports unterminated string literal errors + // and generates a synthetic string token with non-zero length. + // Because of this, check for length > 0 rather than !isSynthetic. + if (endToken.type == TokenType.EOF || endToken.length > 0) { + break; + } + endToken = endToken.previous; + } + var end = endToken.end; + var start = node.offset; + if (end <= _startOffset || start > _endOffset) { + return; + } + // Check children. + try { + node.visitChildren(this); + } catch (_) { + // Ignore the exception and proceed in order to visit the rest of the + // structure. + } + // Found a child. + if (_foundNode != null) { + return; + } + // Check this node. + if (start <= _startOffset && _endOffset < end) { + _foundNode = node; + } + } +} + +/// Instances of the class [ConstantEvaluator] evaluate constant expressions to +/// produce their compile-time value. +/// +/// According to the Dart Language Specification: +/// +/// > A constant expression is one of the following: +/// > +/// > * A literal number. +/// > * A literal boolean. +/// > * A literal string where any interpolated expression is a compile-time +/// > constant that evaluates to a numeric, string or boolean value or to +/// > **null**. +/// > * A literal symbol. +/// > * **null**. +/// > * A qualified reference to a static constant variable. +/// > * An identifier expression that denotes a constant variable, class or type +/// > alias. +/// > * A constant constructor invocation. +/// > * A constant list literal. +/// > * A constant map literal. +/// > * A simple or qualified identifier denoting a top-level function or a +/// > static method. +/// > * A parenthesized expression _(e)_ where _e_ is a constant expression. +/// > * +/// > An expression of the form identical(e1, e2) +/// > where e1 and e2 are constant +/// > expressions and identical() is statically bound to the predefined +/// > dart function identical() discussed above. +/// > +/// > * +/// > An expression of one of the forms e1 == e2 +/// > or e1 != e2 where e1 and +/// > e2 are constant expressions that evaluate to a +/// > numeric, string or boolean value. +/// > +/// > * +/// > An expression of one of the forms !e, e1 && +/// > e2 or e1 || e2, where +/// > e, e1 and e2 are constant +/// > expressions that evaluate to a boolean value. +/// > +/// > * +/// > An expression of one of the forms ~e, e1 ^ +/// > e2, e1 & e2, +/// > e1 | e2, e1 >> +/// > e2 or e1 << e2, where +/// > e, e1 and e2 are constant +/// > expressions that evaluate to an integer value or to null. +/// > +/// > * +/// > An expression of one of the forms -e, e1 +/// > -e2, e1 * e2, +/// > e1 / e2, e1 ~/ +/// > e2, e1 > e2, +/// > e1 < e2, e1 >= +/// > e2, e1 <= e2 or +/// > e1 % e2, where e, +/// > e1 and e2 are constant expressions +/// > that evaluate to a numeric value or to null. +/// > +/// > * +/// > An expression of one the form e1 + e2, +/// > e1 -e2 where e1 and +/// > e2 are constant expressions that evaluate to a numeric or +/// > string value or to null. +/// > +/// > * +/// > An expression of the form e1 ? e2 : +/// > e3 where e1, e2 and +/// > e3 are constant expressions, and e1 +/// > evaluates to a boolean value. +/// > +/// +/// However, this comment is now at least a little bit out of sync with the +/// spec. +/// +/// The values returned by instances of this class are therefore `null` and +/// instances of the classes `Boolean`, `BigInteger`, `Double`, `String`, and +/// `DartObject`. +/// +/// In addition, this class defines several values that can be returned to +/// indicate various conditions encountered during evaluation. These are +/// documented with the static fields that define those values. +class ConstantEvaluator extends GeneralizingAstVisitor { + /// The value returned for expressions (or non-expression nodes) that are not + /// compile-time constant expressions. + static Object NOT_A_CONSTANT = Object(); + + @override + Object visitAdjacentStrings(AdjacentStrings node) { + final buffer = StringBuffer(); + for (final string in node.strings) { + final value = string.accept(this); + if (identical(value, NOT_A_CONSTANT)) { + return value; + } + buffer.write(value); + } + return buffer.toString(); + } + + @override + Object visitBinaryExpression(BinaryExpression node) { + final leftOperand = node.leftOperand.accept(this); + if (identical(leftOperand, NOT_A_CONSTANT)) { + return leftOperand; + } + final rightOperand = node.rightOperand.accept(this); + if (identical(rightOperand, NOT_A_CONSTANT)) { + return rightOperand; + } + while (true) { + if (node.operator.type == TokenType.AMPERSAND) { + // integer or {@code null} + if (leftOperand is int && rightOperand is int) { + return leftOperand & rightOperand; + } + } else if (node.operator.type == TokenType.AMPERSAND_AMPERSAND) { + // boolean or {@code null} + if (leftOperand is bool && rightOperand is bool) { + return leftOperand && rightOperand; + } + } else if (node.operator.type == TokenType.BANG_EQ) { + // numeric, string, boolean, or {@code null} + if (leftOperand is bool && rightOperand is bool) { + return leftOperand != rightOperand; + } else if (leftOperand is num && rightOperand is num) { + return leftOperand != rightOperand; + } else if (leftOperand is String && rightOperand is String) { + return leftOperand != rightOperand; + } + } else if (node.operator.type == TokenType.BAR) { + // integer or {@code null} + if (leftOperand is int && rightOperand is int) { + return leftOperand | rightOperand; + } + } else if (node.operator.type == TokenType.BAR_BAR) { + // boolean or {@code null} + if (leftOperand is bool && rightOperand is bool) { + return leftOperand || rightOperand; + } + } else if (node.operator.type == TokenType.CARET) { + // integer or {@code null} + if (leftOperand is int && rightOperand is int) { + return leftOperand ^ rightOperand; + } + } else if (node.operator.type == TokenType.EQ_EQ) { + // numeric, string, boolean, or {@code null} + if (leftOperand is bool && rightOperand is bool) { + return leftOperand == rightOperand; + } else if (leftOperand is num && rightOperand is num) { + return leftOperand == rightOperand; + } else if (leftOperand is String && rightOperand is String) { + return leftOperand == rightOperand; + } + } else if (node.operator.type == TokenType.GT) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand.compareTo(rightOperand) > 0; + } + } else if (node.operator.type == TokenType.GT_EQ) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand.compareTo(rightOperand) >= 0; + } + } else if (node.operator.type == TokenType.GT_GT) { + // integer or {@code null} + if (leftOperand is int && rightOperand is int) { + return leftOperand >> rightOperand; + } + } else if (node.operator.type == TokenType.LT) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand.compareTo(rightOperand) < 0; + } + } else if (node.operator.type == TokenType.LT_EQ) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand.compareTo(rightOperand) <= 0; + } + } else if (node.operator.type == TokenType.LT_LT) { + // integer or {@code null} + if (leftOperand is int && rightOperand is int) { + return leftOperand << rightOperand; + } + } else if (node.operator.type == TokenType.MINUS) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand - rightOperand; + } + } else if (node.operator.type == TokenType.PERCENT) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand.remainder(rightOperand); + } + } else if (node.operator.type == TokenType.PLUS) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand + rightOperand; + } + if (leftOperand is String && rightOperand is String) { + return leftOperand + rightOperand; + } + } else if (node.operator.type == TokenType.STAR) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand * rightOperand; + } + } else if (node.operator.type == TokenType.SLASH) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand / rightOperand; + } + } else if (node.operator.type == TokenType.TILDE_SLASH) { + // numeric or {@code null} + if (leftOperand is num && rightOperand is num) { + return leftOperand ~/ rightOperand; + } + } + break; + } + // TODO(brianwilkerson) This doesn't handle numeric conversions. + return visitExpression(node); + } + + @override + Object visitBooleanLiteral(BooleanLiteral node) => node.value; + + @override + Object visitDoubleLiteral(DoubleLiteral node) => node.value; + + @override + Object visitIntegerLiteral(IntegerLiteral node) => node.value; + + @override + Object visitInterpolationExpression(InterpolationExpression node) { + final value = node.expression.accept(this); + if (value == null || value is bool || value is String || value is num) { + return value; + } + return NOT_A_CONSTANT; + } + + @override + Object visitInterpolationString(InterpolationString node) => node.value; + + @override + Object visitListLiteral(ListLiteral node) { + final list = []; + for (final element in node.elements) { + if (element is Expression) { + final value = element.accept(this); + if (identical(value, NOT_A_CONSTANT)) { + return value; + } + list.add(value); + } else { + // There are a lot of constants that this class does not support, so we + // didn't add support for the extended collection support. + return NOT_A_CONSTANT; + } + } + return list; + } + + @override + Object visitMethodInvocation(MethodInvocation node) => visitNode(node); + + @override + Object visitNode(AstNode node) => NOT_A_CONSTANT; + + @override + Object visitNullLiteral(NullLiteral node) => null; + + @override + Object visitParenthesizedExpression(ParenthesizedExpression node) => node.expression.accept(this); + + @override + Object visitPrefixedIdentifier(PrefixedIdentifier node) => _getConstantValue(null); + + @override + Object visitPrefixExpression(PrefixExpression node) { + final operand = node.operand.accept(this); + if (identical(operand, NOT_A_CONSTANT)) { + return operand; + } + while (true) { + if (node.operator.type == TokenType.BANG) { + if (identical(operand, true)) { + return false; + } else if (identical(operand, false)) { + return true; + } + } else if (node.operator.type == TokenType.TILDE) { + if (operand is int) { + return ~operand; + } + } else if (node.operator.type == TokenType.MINUS) { + if (operand == null) { + return null; + } else if (operand is num) { + return -operand; + } + } else {} + break; + } + return NOT_A_CONSTANT; + } + + @override + Object visitPropertyAccess(PropertyAccess node) => _getConstantValue(null); + + @override + Object visitSetOrMapLiteral(SetOrMapLiteral node) { + // There are a lot of constants that this class does not support, so we + // didn't add support for set literals. As a result, this assumes that we're + // looking at a map literal until we prove otherwise. + Map map = HashMap(); + for (final element in node.elements) { + if (element is MapLiteralEntry) { + final key = element.key.accept(this); + final value = element.value.accept(this); + if (key is String && !identical(value, NOT_A_CONSTANT)) { + map[key] = value; + } else { + return NOT_A_CONSTANT; + } + } else { + // There are a lot of constants that this class does not support, so + // we didn't add support for the extended collection support. + return NOT_A_CONSTANT; + } + } + return map; + } + + @override + Object visitSimpleIdentifier(SimpleIdentifier node) => _getConstantValue(null); + + @override + Object visitSimpleStringLiteral(SimpleStringLiteral node) => node.value; + + @override + Object visitStringInterpolation(StringInterpolation node) { + final buffer = StringBuffer(); + for (final element in node.elements) { + final value = element.accept(this); + if (identical(value, NOT_A_CONSTANT)) { + return value; + } + buffer.write(value); + } + return buffer.toString(); + } + + @override + Object visitSymbolLiteral(SymbolLiteral node) { + // TODO(brianwilkerson) This isn't optimal because a Symbol is not a String. + final buffer = StringBuffer(); + for (final component in node.components) { + if (buffer.length > 0) { + buffer.writeCharCode(0x2E); + } + buffer.write(component.lexeme); + } + return buffer.toString(); + } + + /// Return the constant value of the static constant represented by the given + /// [element]. + Object _getConstantValue(Element element) { + // TODO(brianwilkerson) Implement this +// if (element is FieldElement) { +// FieldElement field = element; +// if (field.isStatic && field.isConst) { +// //field.getConstantValue(); +// } +// // } else if (element instanceof VariableElement) { +// // VariableElement variable = (VariableElement) element; +// // if (variable.isStatic() && variable.isConst()) { +// // //variable.getConstantValue(); +// // } +// } + return NOT_A_CONSTANT; + } +} diff --git a/tools/analyzer_plugin/lib/src/util/ast_util.dart b/tools/analyzer_plugin/lib/src/util/ast_util.dart index f694a5583..76577c46e 100644 --- a/tools/analyzer_plugin/lib/src/util/ast_util.dart +++ b/tools/analyzer_plugin/lib/src/util/ast_util.dart @@ -3,15 +3,12 @@ library over_react_analyzer_plugin.src.ast_util; import 'dart:collection'; import 'dart:mirrors'; -// This is necessary for `ConstantEvaluator`. If that API is removed, it can just -// be copied and pasted into this analyzer package (if still needed). -// ignore: deprecated_member_use -import 'package:analyzer/analyzer.dart' show ConstantEvaluator; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/source/line_info.dart'; +import 'package:over_react_analyzer_plugin/src/util/analyzer_util.dart'; export 'package:over_react/src/builder/parsing/ast_util.dart'; export 'package:over_react/src/builder/parsing/util.dart'; diff --git a/tools/analyzer_plugin/pubspec.yaml b/tools/analyzer_plugin/pubspec.yaml index 35f0b5fbe..45e665814 100644 --- a/tools/analyzer_plugin/pubspec.yaml +++ b/tools/analyzer_plugin/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: ">=2.11.0 <3.0.0" dependencies: analyzer: ">=0.39.10 <0.42.0" - analyzer_plugin: '^0.2.4' + analyzer_plugin: ">=0.2.4 <0.5.0" collection: ^1.14.0 # Upon release, this should be pinned to the over_react version from ../../pubspec.yaml # so that it always resolves to the same version of over_react that the user has pulled in, @@ -28,7 +28,6 @@ dev_dependencies: io: ^0.3.2+1 logging: ^0.11.3+2 markdown: ^2.1.5 - mockito: ^4.1.1 package_config: ^1.9.3 test: ^1.15.7 test_reflective_loader: ^0.1.9 diff --git a/tools/analyzer_plugin/test/integration/mocks.dart b/tools/analyzer_plugin/test/integration/stubs.dart similarity index 91% rename from tools/analyzer_plugin/test/integration/mocks.dart rename to tools/analyzer_plugin/test/integration/stubs.dart index ac637938e..8b15333b7 100644 --- a/tools/analyzer_plugin/test/integration/mocks.dart +++ b/tools/analyzer_plugin/test/integration/stubs.dart @@ -4,18 +4,20 @@ import 'package:analyzer_plugin/channel/channel.dart'; import 'package:analyzer_plugin/plugin/plugin.dart'; import 'package:analyzer_plugin/protocol/protocol.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; -import 'package:mockito/mockito.dart'; import 'package:over_react_analyzer_plugin/src/plugin.dart'; import 'test_bases/assist_test_base.dart'; -class MockChannel extends Mock implements PluginCommunicationChannel { +class StubChannel implements PluginCommunicationChannel { final List sentNotifications = []; @override void sendNotification(Notification notification) { sentNotifications.add(notification); } + + @override + dynamic noSuchMethod(Invocation invocation) {} } /// A concrete [ServerPlugin] implementation designed for use in testing a diff --git a/tools/analyzer_plugin/test/integration/test_bases/server_plugin_contributor_test_base.dart b/tools/analyzer_plugin/test/integration/test_bases/server_plugin_contributor_test_base.dart index 67beee48e..0a104620f 100644 --- a/tools/analyzer_plugin/test/integration/test_bases/server_plugin_contributor_test_base.dart +++ b/tools/analyzer_plugin/test/integration/test_bases/server_plugin_contributor_test_base.dart @@ -5,7 +5,7 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:test/test.dart'; -import '../mocks.dart'; +import '../stubs.dart'; import 'analysis_driver_test_base.dart'; import 'assist_test_base.dart'; @@ -99,7 +99,7 @@ abstract class ServerPluginContributorTestBase extends AnalysisDriverTestBase { reason: 'Unexpected plugin error(s):\n${pluginErrors.map((e) => e.toJson()).join('\n')}'); } - MockChannel _channel; + StubChannel _channel; PluginForTest _plugin; @override @@ -107,7 +107,7 @@ abstract class ServerPluginContributorTestBase extends AnalysisDriverTestBase { Future setUp() async { await super.setUp(); - _channel = MockChannel(); + _channel = StubChannel(); _plugin = PluginForTest(analysisDriver, resourceProvider)..start(_channel); // ignore: missing_required_param diff --git a/tools/analyzer_plugin/test/test_util.dart b/tools/analyzer_plugin/test/test_util.dart index ca58bfd8e..821f593a0 100644 --- a/tools/analyzer_plugin/test/test_util.dart +++ b/tools/analyzer_plugin/test/test_util.dart @@ -121,7 +121,7 @@ Future> parseAndGetResolvedUnits(Map IgnoreInfo.calculateIgnores(result.content, lineInfo)); + filterIgnores(result.errors, lineInfo, () => IgnoreInfo.forDart(result.unit, result.content)); if (filteredErrors.isNotEmpty) { throw ArgumentError('Parse errors in source "$path":\n${filteredErrors.join('\n')}'); } diff --git a/tools/analyzer_plugin/test/unit/util/analyzer_util_test.dart b/tools/analyzer_plugin/test/unit/util/analyzer_util_test.dart new file mode 100644 index 000000000..5010c05c9 --- /dev/null +++ b/tools/analyzer_plugin/test/unit/util/analyzer_util_test.dart @@ -0,0 +1,137 @@ +// Adapted from: https://github.com/dart-lang/sdk/blob/1601f6fcd2a7aa39bdd71f1f988fe7acfaecc418/pkg/analyzer/test/src/dart/ast/utilities_test.dart +// +// Copyright 2013, the Dart project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:over_react_analyzer_plugin/src/util/analyzer_util.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../test_util.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NodeLocatorTest); + defineReflectiveTests(NodeLocator2Test); + }); +} + +@reflectiveTest +class NodeLocator2Test { + void test_onlyStartOffset() { + const code = ' int vv; '; + // 012345678 + final unit = parseAndGetUnit(code); + final declaration = unit.declarations[0] as TopLevelVariableDeclaration; + final variableList = declaration.variables; + final typeName = (variableList.type as TypeName).name; + final varName = variableList.variables[0].name; + expect(NodeLocator2(0).searchWithin(unit), same(unit)); + expect(NodeLocator2(1).searchWithin(unit), same(typeName)); + expect(NodeLocator2(2).searchWithin(unit), same(typeName)); + expect(NodeLocator2(3).searchWithin(unit), same(typeName)); + expect(NodeLocator2(4).searchWithin(unit), same(variableList)); + expect(NodeLocator2(5).searchWithin(unit), same(varName)); + expect(NodeLocator2(6).searchWithin(unit), same(varName)); + expect(NodeLocator2(7).searchWithin(unit), same(declaration)); + expect(NodeLocator2(8).searchWithin(unit), same(unit)); + expect(NodeLocator2(9).searchWithin(unit), isNull); + expect(NodeLocator2(100).searchWithin(unit), isNull); + } + + void test_startEndOffset() { + const code = ' int vv; '; + // 012345678 + final unit = parseAndGetUnit(code); + final declaration = unit.declarations[0] as TopLevelVariableDeclaration; + final variableList = declaration.variables; + final typeName = (variableList.type as TypeName).name; + final varName = variableList.variables[0].name; + expect(NodeLocator2(-1, 2).searchWithin(unit), isNull); + expect(NodeLocator2(0, 2).searchWithin(unit), same(unit)); + expect(NodeLocator2(1, 2).searchWithin(unit), same(typeName)); + expect(NodeLocator2(1, 3).searchWithin(unit), same(typeName)); + expect(NodeLocator2(1, 4).searchWithin(unit), same(variableList)); + expect(NodeLocator2(5, 6).searchWithin(unit), same(varName)); + expect(NodeLocator2(5, 7).searchWithin(unit), same(declaration)); + expect(NodeLocator2(5, 8).searchWithin(unit), same(unit)); + expect(NodeLocator2(5, 100).searchWithin(unit), isNull); + expect(NodeLocator2(100, 200).searchWithin(unit), isNull); + } +} + +@reflectiveTest +class NodeLocatorTest { + void test_range() { + final unit = parseAndGetUnit("library myLib;"); + final node = _assertLocate(unit, 4, 10); + expect(node, isA()); + } + + void test_searchWithin_null() { + final locator = NodeLocator(0, 0); + expect(locator.searchWithin(null), isNull); + } + + void test_searchWithin_offset() { + final unit = parseAndGetUnit("library myLib;"); + final node = _assertLocate(unit, 10, 10); + expect(node, isA()); + } + + void test_searchWithin_offsetAfterNode() { + final unit = parseAndGetUnit(r''' +class A {} +class B {}'''); + final locator = NodeLocator(1024, 1024); + final node = locator.searchWithin(unit.declarations[0]); + expect(node, isNull); + } + + void test_searchWithin_offsetBeforeNode() { + final unit = parseAndGetUnit(r''' +class A {} +class B {}'''); + final locator = NodeLocator(0, 0); + final node = locator.searchWithin(unit.declarations[1]); + expect(node, isNull); + } + + AstNode _assertLocate( + CompilationUnit unit, + int start, + int end, + ) { + final locator = NodeLocator(start, end); + final node = locator.searchWithin(unit); + expect(locator.foundNode, same(node)); + expect(node.offset <= start, isTrue, reason: "Node starts after range"); + expect(node.offset + node.length > end, isTrue, reason: "Node ends before range"); + return node; + } +} diff --git a/tools/analyzer_plugin/tool/travis.sh b/tools/analyzer_plugin/tool/travis.sh deleted file mode 100755 index 1d7beda91..000000000 --- a/tools/analyzer_plugin/tool/travis.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# ################################################################################## -# -# Runs all analyzer plugin tasks that should be run in the parent over_react CI -# -# ################################################################################## - -# Fail any script if any unexpected errors occur in the subsequent commands -set -e - -pub get -pub run dart_dev format --check -pub run dart_dev analyze -pub run dependency_validator --no-fatal-pins -pub run dart_dev test