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),
+    ]);
+  }
+}