check for undefined parameter references in `UseResult.unless` targets
Change-Id: Iaba4696c3f04efe0c104695ac51896bb81d06bb0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/208862
Commit-Queue: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analyzer/lib/error/error.dart b/pkg/analyzer/lib/error/error.dart
index 0a2b2c8..7d50e7e 100644
--- a/pkg/analyzer/lib/error/error.dart
+++ b/pkg/analyzer/lib/error/error.dart
@@ -604,6 +604,7 @@
HintCode.TYPE_CHECK_IS_NOT_NULL,
HintCode.TYPE_CHECK_IS_NULL,
HintCode.UNDEFINED_HIDDEN_NAME,
+ HintCode.UNDEFINED_REFERENCED_PARAMETER,
HintCode.UNDEFINED_SHOWN_NAME,
HintCode.UNIGNORABLE_IGNORE,
HintCode.UNNECESSARY_CAST,
diff --git a/pkg/analyzer/lib/src/dart/error/hint_codes.dart b/pkg/analyzer/lib/src/dart/error/hint_codes.dart
index 4030bff..afaad1f 100644
--- a/pkg/analyzer/lib/src/dart/error/hint_codes.dart
+++ b/pkg/analyzer/lib/src/dart/error/hint_codes.dart
@@ -2878,6 +2878,18 @@
hasPublishedDocs: true);
/**
+ * This hint is generated when an `@UnusedResult.unless` annotation
+ * references an undefined parameter.
+ *
+ * Parameters:
+ * 0: the name of the undefined parameter
+ * 1: the name of the targeted member
+ */
+ static const HintCode UNDEFINED_REFERENCED_PARAMETER = HintCode(
+ 'UNDEFINED_REFERENCED_PARAMETER',
+ "The parameter '{0}' is not defined by '{1}'.");
+
+ /**
* Parameters:
* 0: the name of the library being imported
* 1: the name in the show clause that isn't defined in the library
diff --git a/pkg/analyzer/lib/src/error/best_practices_verifier.dart b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
index c0f023b..5425f5e 100644
--- a/pkg/analyzer/lib/src/error/best_practices_verifier.dart
+++ b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
@@ -12,6 +12,7 @@
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
@@ -286,7 +287,28 @@
}
}
- // todo(pq): add validation to ensure `UseResult.unless(parameterDefined: 'p')` only targets members that declare a parameter `p`.
+ // Check for a reference to an undefined parameter in a `@UseResult.unless`
+ // annotation.
+ if (element.isUseResult) {
+ var undefinedParam = _findUndefinedUseResultParam(element, node, parent);
+ if (undefinedParam != null) {
+ String? name;
+ if (parent is FunctionDeclaration) {
+ name = parent.name.name;
+ } else if (parent is MethodDeclaration) {
+ name = parent.name.name;
+ }
+ if (name != null) {
+ var paramName = undefinedParam is SimpleStringLiteral
+ ? undefinedParam.value
+ : undefinedParam.staticParameterElement?.name;
+ _errorReporter.reportErrorForNode(
+ HintCode.UNDEFINED_REFERENCED_PARAMETER,
+ undefinedParam,
+ [paramName ?? undefinedParam, name]);
+ }
+ }
+ }
var kinds = _targetKindsFor(element);
if (kinds.isNotEmpty) {
@@ -1541,6 +1563,62 @@
}
}
+ Expression? _findUndefinedUseResultParam(
+ ElementAnnotation element, Annotation node, AstNode parent) {
+ var constructorName = node.name;
+ if (constructorName is! PrefixedIdentifier ||
+ constructorName.identifier.name != 'unless') {
+ return null;
+ }
+
+ var unlessParam = element
+ .computeConstantValue()
+ ?.getField('parameterDefined')
+ ?.toStringValue();
+ if (unlessParam == null) {
+ return null;
+ }
+
+ Expression? checkParams(FormalParameterList? parameterList) {
+ if (parameterList == null) {
+ return null;
+ }
+
+ for (var param in parameterList.parameters) {
+ if (param is FormalParameter) {
+ // Param is defined.
+ if (param.identifier?.name == unlessParam) {
+ return null;
+ }
+ }
+ }
+
+ // Find and return the parameter value node.
+ var arguments = node.arguments?.arguments;
+ if (arguments == null) {
+ return null;
+ }
+
+ for (var arg in arguments) {
+ if (arg is NamedExpression &&
+ arg.name.label.name == 'parameterDefined') {
+ return arg.expression;
+ }
+ }
+
+ return null;
+ }
+
+ if (parent is FunctionDeclarationImpl) {
+ return checkParams(parent.functionExpression.parameters);
+ }
+ if (parent is MethodDeclarationImpl) {
+ return checkParams(parent.parameters);
+ }
+
+ return null;
+ }
+
/// Return subexpressions that are marked `@doNotStore`, as a map so that
/// corresponding elements can be used in the diagnostic message.
Map<Expression, Element> _getSubExpressionsMarkedDoNotStore(
diff --git a/pkg/analyzer/test/src/diagnostics/test_all.dart b/pkg/analyzer/test/src/diagnostics/test_all.dart
index 49fbffe..176cb48 100644
--- a/pkg/analyzer/test/src/diagnostics/test_all.dart
+++ b/pkg/analyzer/test/src/diagnostics/test_all.dart
@@ -667,6 +667,8 @@
import 'undefined_named_parameter_test.dart' as undefined_named_parameter;
import 'undefined_operator_test.dart' as undefined_operator;
import 'undefined_prefixed_name_test.dart' as undefined_prefixed_name;
+import 'undefined_referenced_parameter_test.dart'
+ as undefined_referenced_parameter;
import 'undefined_setter_test.dart' as undefined_setter;
import 'undefined_shown_name_test.dart' as undefined_shown_name;
import 'unignorable_ignore_test.dart' as unignorable_ignore;
@@ -1158,6 +1160,7 @@
undefined_named_parameter.main();
undefined_operator.main();
undefined_prefixed_name.main();
+ undefined_referenced_parameter.main();
undefined_setter.main();
undefined_shown_name.main();
unignorable_ignore.main();
diff --git a/pkg/analyzer/test/src/diagnostics/undefined_referenced_parameter_test.dart b/pkg/analyzer/test/src/diagnostics/undefined_referenced_parameter_test.dart
new file mode 100644
index 0000000..c34634c
--- /dev/null
+++ b/pkg/analyzer/test/src/diagnostics/undefined_referenced_parameter_test.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2021, 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/src/error/codes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dart/resolution/context_collection_resolution.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(UndefinedReferencedParameterTest);
+ });
+}
+
+@reflectiveTest
+class UndefinedReferencedParameterTest extends PubPackageResolutionTest {
+ @override
+ void setUp() {
+ super.setUp();
+ writeTestPackageConfigWithMeta();
+ }
+
+ test_method() async {
+ await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+class Foo {
+ @UseResult.unless(parameterDefined: 'undef')
+ int foo([int? value]) => value ?? 0;
+}
+''', [
+ error(HintCode.UNDEFINED_REFERENCED_PARAMETER, 84, 7),
+ ]);
+ }
+
+ test_method_parameterDefined() async {
+ await assertNoErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+class Foo {
+ @UseResult.unless(parameterDefined: 'value')
+ int foo([int? value]) => value ?? 0;
+}
+''');
+ }
+
+ test_topLevelFunction() async {
+ await assertErrorsInCode(r'''
+import 'package:meta/meta.dart';
+
+@UseResult.unless(parameterDefined: 'undef')
+int foo([int? value]) => value ?? 0;
+''', [
+ error(HintCode.UNDEFINED_REFERENCED_PARAMETER, 70, 7),
+ ]);
+ }
+}