Allow a simple override to make a @protected method public (#3482)

diff --git a/lib/src/rules/unnecessary_overrides.dart b/lib/src/rules/unnecessary_overrides.dart
index 7f739aa..3d2cb6a 100644
--- a/lib/src/rules/unnecessary_overrides.dart
+++ b/lib/src/rules/unnecessary_overrides.dart
@@ -40,10 +40,11 @@
 It's valid to override a member in the following cases:
 
 * if a type (return type or a parameter type) is not the exactly the same as the
-super method,
+  super member,
 * if the `covariant` keyword is added to one of the parameters,
 * if documentation comments are present on the member,
-* if the member has annotations other than `@override`.
+* if the member has annotations other than `@override`,
+* if the member is not annotated with `@protected`, and the super member is.
 
 `noSuchMethod` is a special method and is not checked by this rule.
 
@@ -68,7 +69,9 @@
 abstract class _AbstractUnnecessaryOverrideVisitor extends SimpleAstVisitor {
   final LintRule rule;
 
-  ExecutableElement? inheritedMethod;
+  /// If [declaration] is an inherited member of interest, then this is set in
+  /// [visitMethodDeclaration].
+  late ExecutableElement _inheritedMethod;
   late MethodDeclaration declaration;
 
   _AbstractUnnecessaryOverrideVisitor(this.rule);
@@ -99,17 +102,27 @@
 
   @override
   void visitMethodDeclaration(MethodDeclaration node) {
-    // noSuchMethod is mandatory to proxify
+    // 'noSuchMethod' is mandatory to proxify.
     if (node.name.name == 'noSuchMethod') return;
 
-    // it's ok to override to have better documentation
+    // It's ok to override to have better documentation.
     if (node.documentationComment != null) return;
 
-    inheritedMethod = getInheritedElement(node);
+    var inheritedMethod = getInheritedElement(node);
+    if (inheritedMethod == null) return;
+    _inheritedMethod = inheritedMethod;
     declaration = node;
-    if (inheritedMethod != null && !_addsMetadata() && _haveSameDeclaration()) {
-      node.body.accept(this);
-    }
+
+    // It's ok to override to add annotations.
+    if (_addsMetadata()) return;
+
+    // It's ok to override to change the signature.
+    if (!_haveSameDeclaration()) return;
+
+    // It's ok to override to make a `@protected` method public.
+    if (_makesPublicFromProtected()) return;
+
+    node.body.accept(this);
   }
 
   @override
@@ -127,36 +140,50 @@
     rule.reportLint(declaration.name);
   }
 
+  /// Returns whether [declaration] is annotated with any metadata (other than
+  /// `@override` or `@Override`).
   bool _addsMetadata() {
     var metadata = declaration.declaredElement?.metadata;
     if (metadata != null) {
       for (var annotation in metadata) {
-        if (!annotation.isOverride) {
-          return true;
-        }
+        if (annotation.isOverride) continue;
+        if (annotation.isProtected && _inheritedMethod.hasProtected) continue;
+
+        // Any other annotation implies a meaningful override.
+        return true;
       }
     }
     return false;
   }
 
+  /// Returns true if [_inheritedMethod] is `@protected` and [declaration] is
+  /// not `@protected`, and false otherwise.
+  ///
+  /// This indicates that [_inheritedMethod] may have been overridden in order
+  /// to expand its visibility.
+  bool _makesPublicFromProtected() {
+    var declaredElement = declaration.declaredElement;
+    if (declaredElement == null) return false;
+    if (declaredElement.hasProtected) {
+      return false;
+    }
+    return _inheritedMethod.hasProtected;
+  }
+
   bool _haveSameDeclaration() {
     var declaredElement = declaration.declaredElement;
     if (declaredElement == null) {
       return false;
     }
-    var inheritedMethod = this.inheritedMethod;
-    if (inheritedMethod == null) {
-      return false;
-    }
-    if (declaredElement.returnType != inheritedMethod.returnType) {
+    if (declaredElement.returnType != _inheritedMethod.returnType) {
       return false;
     }
     if (declaredElement.parameters.length !=
-        inheritedMethod.parameters.length) {
+        _inheritedMethod.parameters.length) {
       return false;
     }
-    for (var i = 0; i < inheritedMethod.parameters.length; i++) {
-      var superParam = inheritedMethod.parameters[i];
+    for (var i = 0; i < _inheritedMethod.parameters.length; i++) {
+      var superParam = _inheritedMethod.parameters[i];
       var param = declaredElement.parameters[i];
       if (param.type != superParam.type) return false;
       if (param.name != superParam.name) return false;
@@ -189,7 +216,7 @@
 
   @override
   void visitPropertyAccess(PropertyAccess node) {
-    if (node.propertyName.staticElement == inheritedMethod) {
+    if (node.propertyName.staticElement == _inheritedMethod) {
       node.target?.accept(this);
     }
   }
@@ -207,7 +234,7 @@
   void visitMethodInvocation(MethodInvocation node) {
     var declarationParameters = declaration.parameters;
     if (declarationParameters != null &&
-        node.methodName.staticElement == inheritedMethod &&
+        node.methodName.staticElement == _inheritedMethod &&
         DartTypeUtilities.matchesArgumentsWithParameters(
             node.argumentList.arguments, declarationParameters.parameters)) {
       node.target?.accept(this);
@@ -271,7 +298,7 @@
                 node.rightHandSide)) {
       var leftPart = node.leftHandSide.unParenthesized;
       if (leftPart is PropertyAccess) {
-        if (node.writeElement == inheritedMethod) {
+        if (node.writeElement == _inheritedMethod) {
           leftPart.target?.accept(this);
         }
       }
diff --git a/test_data/rules/unnecessary_overrides.dart b/test_data/rules/unnecessary_overrides.dart
index 7b852e8..1314100 100644
--- a/test_data/rules/unnecessary_overrides.dart
+++ b/test_data/rules/unnecessary_overrides.dart
@@ -4,6 +4,8 @@
 
 // test w/ `dart test -N unnecessary_overrides`
 
+import 'package:meta/meta.dart';
+
 class _MyAnnotation {
   const _MyAnnotation();
 }
@@ -122,7 +124,9 @@
 class C {
   num get g => 0;
   set s(int v) {}
+  @protected
   num m(int v) => 0;
+  @protected
   num m1({int v = 20}) => 0;
   num m2([int v = 20]) => 0;
   num operator +(int other) => 0;
@@ -187,6 +191,12 @@
   @override
   num m2([int v = 10]) => super.m2(v); // OK
 }
+class ProtectedMadePublic extends C {
+  @override
+  num m(int v) => super.m(v); // OK
+  @protected
+  num m1({int v = 20}) => super.m1(v: v); // LINT
+}
 
 // noSuchMethod is allowed to proxify
 class D implements C {