[analysis] Reland "Added initial implementation of the flutter_analyzer_plugin (#175679)" (#191022)
Relands #175679 (reverted in #179766).
Fixes https://github.com/flutter/flutter/issues/175276.
### Context & Fixes
The original PR was reverted because the `Linux linux_unopt` engine
builder timed out during postsubmit.
Root cause & changes in this reland:
1. **Scoped Plugin Configuration**: Plugin activation is now configured
directly in target package `analysis_options.yaml` files
(`packages/flutter/lib`, `packages/flutter/test`,
`packages/flutter_tools`) rather than at the monorepo root
`analysis_options.yaml`. This avoids leaking plugin configuration into
subtrees such as `engine/src/flutter` where standard pub resolution and
framework plugin rules do not apply.
2. **Analyzer AST Modernization**: Updated `ClassDeclaration` pattern
matching in `render_box_intrinsics.dart` from `name` to `namePart` for
compatibility with `package:analyzer` 10.1+.
3. **State Element Caching**: Updated `isPublicStateSubtype` in
`protect_public_state_subtypes.dart` to cache `InterfaceElement` rather
than instantiated `DartType`, ensuring all `State<T>` subclasses are
properly analyzed regardless of generic type arguments.
4. **Defensive Guard**: Added null check on `ConstructorElement` in
`no_stopwatches.dart`.
5. **Path & Test Fixes**: Corrected relative plugin path in
`packages/flutter/test/analysis_options.yaml` and test method naming in
`no_stopwatches_test.dart`.
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] All existing and new tests are passing.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 74a5e1e..f4a11f8 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,6 +1,14 @@
include: analysis_options_common.yaml
+# Note: Analyzer plugins (such as flutter_analyzer_plugin) are intentionally
+# configured directly in subpackages (e.g. packages/flutter/lib,
+# packages/flutter/test, packages/flutter_tools) rather than here at the root.
+# This prevents plugins from leaking into excluded or standalone subtrees (like
+# engine/src/flutter) that do not use the framework plugin.
+
analyzer:
+ errors:
+ plugins_in_inner_options: ignore
exclude:
- "bin/cache/**"
# Ignore protoc generated files
diff --git a/dev/bots/suite_runners/run_framework_tests.dart b/dev/bots/suite_runners/run_framework_tests.dart
index 1151980..4376a2c 100644
--- a/dev/bots/suite_runners/run_framework_tests.dart
+++ b/dev/bots/suite_runners/run_framework_tests.dart
@@ -301,6 +301,7 @@
path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'),
fatalWarnings: false,
);
+ await runDartTest(path.join(flutterRoot, 'dev', 'flutter_analyzer_plugin'));
await runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'ui'));
await runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
await runFlutterTest(path.join(flutterRoot, 'dev', 'tools'));
diff --git a/dev/bots/test/analyze-test-input/root/packages/flutter/lib/renderbox_intrinsics.dart b/dev/bots/test/analyze-test-input/root/packages/flutter/lib/renderbox_intrinsics.dart
index 4342be2..23e7330 100644
--- a/dev/bots/test/analyze-test-input/root/packages/flutter/lib/renderbox_intrinsics.dart
+++ b/dev/bots/test/analyze-test-input/root/packages/flutter/lib/renderbox_intrinsics.dart
@@ -2,7 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import '../../foo/fake_render_box.dart';
+abstract class RenderBox {
+ void computeDryBaseline() {}
+ void computeDryLayout() {}
+ void computeDistanceToActualBaseline() {}
+ void computeMaxIntrinsicHeight() {}
+ void computeMinIntrinsicHeight() {}
+ void computeMaxIntrinsicWidth() {}
+ void computeMinIntrinsicWidth() {}
+}
mixin ARenderBoxMixin on RenderBox {
@override
diff --git a/dev/bots/test/analyze-test-input/root/packages/foo/fake_render_box.dart b/dev/bots/test/analyze-test-input/root/packages/foo/fake_render_box.dart
deleted file mode 100644
index 1ed5cc8..0000000
--- a/dev/bots/test/analyze-test-input/root/packages/foo/fake_render_box.dart
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright 2014 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-abstract class RenderBox {
- void computeDryBaseline() {}
- void computeDryLayout() {}
- void computeDistanceToActualBaseline() {}
- void computeMaxIntrinsicHeight() {}
- void computeMinIntrinsicHeight() {}
- void computeMaxIntrinsicWidth() {}
- void computeMinIntrinsicWidth() {}
-}
diff --git a/dev/flutter_analyzer_plugin/.gitignore b/dev/flutter_analyzer_plugin/.gitignore
new file mode 100644
index 0000000..3a85790
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/.gitignore
@@ -0,0 +1,3 @@
+# https://dart.dev/guides/libraries/private-files
+# Created by `dart pub`
+.dart_tool/
diff --git a/dev/flutter_analyzer_plugin/README.md b/dev/flutter_analyzer_plugin/README.md
new file mode 100644
index 0000000..96be4c9
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/README.md
@@ -0,0 +1,7 @@
+# Flutter Analyzer Plugin
+
+This plugin provides custom lint rules specific to development within flutter/flutter.
+
+This plugin is a WIP as cases covered by `dev/bots/analyze.dart` are ported to this plugin,
+with the eventual goal of implementing as many of the checks as possible to reduce the number
+of analysis failures only discovered by CI checks.
diff --git a/dev/flutter_analyzer_plugin/analysis_options.yaml b/dev/flutter_analyzer_plugin/analysis_options.yaml
new file mode 100644
index 0000000..2b89a7d
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/analysis_options.yaml
@@ -0,0 +1,14 @@
+analyzer:
+ exclude:
+ - build/**
+ - android/**
+ - ios/**
+ - web/**
+ - windows/**
+ - macos/**
+ - linux/**
+include: ../analysis_options.yaml
+
+linter:
+ rules:
+ unawaited_futures: true
diff --git a/dev/flutter_analyzer_plugin/lib/main.dart b/dev/flutter_analyzer_plugin/lib/main.dart
new file mode 100644
index 0000000..3426d07
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/lib/main.dart
@@ -0,0 +1,28 @@
+// Copyright 2014 The Flutter Authors. 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:analysis_server_plugin/plugin.dart';
+import 'package:analysis_server_plugin/registry.dart';
+import 'src/rules/avoid_future_catch_error.dart';
+import 'src/rules/no_double_clamp.dart';
+import 'src/rules/no_stopwatches.dart';
+import 'src/rules/protect_public_state_subtypes.dart';
+import 'src/rules/render_box_intrinsics.dart';
+
+final FlutterAnalyzerPlugin plugin = FlutterAnalyzerPlugin();
+
+class FlutterAnalyzerPlugin extends Plugin {
+ @override
+ void register(PluginRegistry registry) {
+ registry
+ ..registerWarningRule(AvoidFutureCatchError())
+ ..registerWarningRule(NoDoubleClamp())
+ ..registerWarningRule(NoStopwatches())
+ ..registerWarningRule(ProtectPublicStateSubtypes())
+ ..registerWarningRule(RenderBoxIntrinsicCalculationRule());
+ }
+
+ @override
+ String get name => 'flutter/flutter analyzer plugin';
+}
diff --git a/dev/flutter_analyzer_plugin/lib/src/rules/avoid_future_catch_error.dart b/dev/flutter_analyzer_plugin/lib/src/rules/avoid_future_catch_error.dart
new file mode 100644
index 0000000..790068e
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/lib/src/rules/avoid_future_catch_error.dart
@@ -0,0 +1,52 @@
+// Copyright 2014 The Flutter Authors. 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/analysis_rule/analysis_rule.dart';
+import 'package:analyzer/analysis_rule/rule_context.dart';
+import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/error/error.dart';
+
+class AvoidFutureCatchError extends AnalysisRule {
+ AvoidFutureCatchError()
+ : super(
+ name: code.name,
+ description: 'Future.catchError and Future.onError are not type safe.',
+ );
+
+ static const LintCode code = LintCode(
+ 'avoid_future_catch_error',
+ 'Avoid using Future.catchError',
+ correctionMessage: 'Use Future.then instead (https://github.com/dart-lang/sdk/issues/51248).',
+ severity: DiagnosticSeverity.ERROR,
+ );
+
+ @override
+ LintCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
+ final visitor = _Visitor(this, context);
+ registry.addMethodInvocation(this, visitor);
+ }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+ _Visitor(this.rule, this.context);
+
+ final AnalysisRule rule;
+ final RuleContext context;
+
+ @override
+ void visitMethodInvocation(MethodInvocation node) {
+ if (node case MethodInvocation(
+ methodName: SimpleIdentifier(name: 'onError' || 'catchError'),
+ realTarget: Expression(staticType: DartType(isDartAsyncFuture: true)),
+ )) {
+ rule.reportAtNode(node);
+ }
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/lib/src/rules/no_double_clamp.dart b/dev/flutter_analyzer_plugin/lib/src/rules/no_double_clamp.dart
new file mode 100644
index 0000000..906b960
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/lib/src/rules/no_double_clamp.dart
@@ -0,0 +1,106 @@
+// Copyright 2014 The Flutter Authors. 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/analysis_rule/analysis_rule.dart';
+import 'package:analyzer/analysis_rule/rule_context.dart';
+import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/error/error.dart';
+
+/// Verify that we use clampDouble instead of double.clamp for performance
+/// reasons.
+///
+/// See also:
+/// * https://github.com/flutter/flutter/pull/103559
+/// * https://github.com/flutter/flutter/issues/103917
+class NoDoubleClamp extends AnalysisRule {
+ NoDoubleClamp()
+ : super(
+ name: code.name,
+ description:
+ 'Verify that we use clampDouble instead of double.clamp for performance reasons.',
+ );
+
+ static const LintCode code = LintCode(
+ 'no_double_clamp',
+ 'Avoid double.clamp for performance reasons.',
+ correctionMessage: 'Use clampDouble instead.',
+ severity: DiagnosticSeverity.ERROR,
+ );
+
+ @override
+ DiagnosticCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
+ final visitor = _Visitor(this, context);
+ registry.addSimpleIdentifier(this, visitor);
+ }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+ _Visitor(this.rule, this.context);
+
+ final AnalysisRule rule;
+ final RuleContext context;
+
+ @override
+ void visitSimpleIdentifier(SimpleIdentifier node) {
+ if (node.name != 'clamp' || node.element is! MethodElement) {
+ return;
+ }
+ final bool isAllowed = switch (node.parent) {
+ // PropertyAccess and PrefixedIdentifier match num.clamp in tear-off form.
+ // Always prefer doubleClamp over tear-offs: even when all 3 operands are
+ // int literals, the return type doesn't get promoted to int:
+ // final x = 1.clamp(0, 2); // The inferred return type is int, where as:
+ // final f = 1.clamp;
+ // final y = f(0, 2) // The inferred return type is num.
+ PropertyAccess(
+ target: Expression(
+ staticType: DartType(isDartCoreDouble: true) ||
+ DartType(isDartCoreNum: true) ||
+ DartType(isDartCoreInt: true),
+ ),
+ ) ||
+ PrefixedIdentifier(
+ prefix: Expression(
+ staticType: DartType(isDartCoreDouble: true) ||
+ DartType(isDartCoreNum: true) ||
+ DartType(isDartCoreInt: true),
+ ),
+ ) => false,
+
+ // Expressions like `final int x = 1.clamp(0, 2);` should be allowed.
+ MethodInvocation(
+ target: Expression(staticType: DartType(isDartCoreInt: true)),
+ argumentList: ArgumentList(
+ arguments: [
+ Expression(staticType: DartType(isDartCoreInt: true)),
+ Expression(staticType: DartType(isDartCoreInt: true)),
+ ],
+ ),
+ ) =>
+ true,
+
+ // Otherwise, disallow num.clamp() invocations.
+ MethodInvocation(
+ target: Expression(
+ staticType: DartType(isDartCoreDouble: true) ||
+ DartType(isDartCoreNum: true) ||
+ DartType(isDartCoreInt: true),
+ ),
+ ) =>
+ false,
+
+ _ => true,
+ };
+ if (!isAllowed) {
+ rule.reportAtNode(node);
+ }
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/lib/src/rules/no_stopwatches.dart b/dev/flutter_analyzer_plugin/lib/src/rules/no_stopwatches.dart
new file mode 100644
index 0000000..4d8fd42
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/lib/src/rules/no_stopwatches.dart
@@ -0,0 +1,130 @@
+// Copyright 2014 The Flutter Authors. 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/analysis_rule/analysis_rule.dart';
+import 'package:analyzer/analysis_rule/rule_context.dart';
+import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/error/error.dart';
+import 'package:path/path.dart' as path;
+
+// The comment pattern representing the "flutter_ignore" inline directive that
+// indicates the line should be exempt from the stopwatch check.
+final Pattern _ignoreStopwatch = RegExp(r'// flutter_ignore: .*stopwatch .*\(see analyze\.dart\)');
+
+/// Use of Stopwatches can introduce test flakes as the logical time of a
+/// stopwatch can fall out of sync with the mocked time of FakeAsync in testing.
+/// The Clock object provides a safe stopwatch instead, which is paired with
+/// FakeAsync as part of the test binding.
+class NoStopwatches extends AnalysisRule {
+ NoStopwatches() : super(name: code.name, description: ruleDescription);
+
+ static const String ruleDescription =
+ 'Use of Stopwatches can introduce test flakes as the logical time of a stopwatch can fall '
+ 'out of sync with the mocked time of FakeAsync in testing.';
+
+ static const LintCode code = LintCode(
+ 'no_stopwatches',
+ ruleDescription,
+ correctionMessage: 'Use clock.stopwatch() from package:clock instead.',
+ severity: DiagnosticSeverity.ERROR,
+ );
+
+ @override
+ DiagnosticCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
+ final visitor = _Visitor(this, context);
+ registry
+ ..addConstructorName(this, visitor)
+ ..addSimpleIdentifier(this, visitor);
+ }
+}
+
+// This visitor finds invocation sites of Stopwatch (and subclasses) constructors
+// and references to "external" functions that return a Stopwatch (and subclasses),
+// including constructors.
+class _Visitor extends SimpleAstVisitor<void> {
+ _Visitor(this.rule, this.context);
+
+ final AnalysisRule rule;
+ final RuleContext context;
+
+ final Map<ClassElement, bool> _isStopwatchClassElementCache = <ClassElement, bool>{};
+
+ bool _checkIfImplementsStopwatchRecursively(ClassElement classElement) {
+ if (classElement.library.isDartCore) {
+ return classElement.name == 'Stopwatch';
+ }
+ return classElement.allSupertypes.any((InterfaceType interface) {
+ final InterfaceElement interfaceElement = interface.element;
+ return interfaceElement is ClassElement && _implementsStopwatch(interfaceElement);
+ });
+ }
+
+ // The cached version, call this method instead of _checkIfImplementsStopwatchRecursively.
+ bool _implementsStopwatch(ClassElement classElement) {
+ return classElement.library.isDartCore
+ ? classElement.name == 'Stopwatch'
+ : _isStopwatchClassElementCache.putIfAbsent(
+ classElement,
+ () => _checkIfImplementsStopwatchRecursively(classElement),
+ );
+ }
+
+ bool _isInternal(LibraryElement libraryElement) {
+ return path.isWithin(
+ libraryElement.session.analysisContext.contextRoot.root.path,
+ libraryElement.firstFragment.source.fullName,
+ );
+ }
+
+ bool _hasTrailingFlutterIgnore(AstNode node) {
+ return context.currentUnit!.content
+ .substring(
+ node.offset + node.length,
+ context.currentUnit!.unit.lineInfo.getOffsetOfLineAfter(node.offset + node.length),
+ )
+ .contains(_ignoreStopwatch);
+ }
+
+ @override
+ void visitConstructorName(ConstructorName node) {
+ final ConstructorElement? element = node.element;
+ if (element == null) {
+ return;
+ }
+ final bool isAllowed = switch (element.returnType) {
+ InterfaceType(element: final ClassElement classElement) =>
+ !_implementsStopwatch(classElement),
+ InterfaceType(element: InterfaceElement()) => true,
+ };
+ if (isAllowed || _hasTrailingFlutterIgnore(node)) {
+ return;
+ }
+ rule.reportAtNode(node);
+ }
+
+ @override
+ void visitSimpleIdentifier(SimpleIdentifier node) {
+ final bool isAllowed = switch (node.element) {
+ ExecutableElement(
+ returnType: InterfaceType(element: final ClassElement classElement),
+ library: final LibraryElement libraryElement,
+ )
+ // Don't double report constructors and factories.
+ when node.element is! ConstructorElement =>
+ _isInternal(libraryElement) || !_implementsStopwatch(classElement),
+ Element() || null => true,
+ };
+ if (isAllowed || _hasTrailingFlutterIgnore(node)) {
+ return;
+ }
+ rule.reportAtNode(node);
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/lib/src/rules/protect_public_state_subtypes.dart b/dev/flutter_analyzer_plugin/lib/src/rules/protect_public_state_subtypes.dart
new file mode 100644
index 0000000..0ec5230
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/lib/src/rules/protect_public_state_subtypes.dart
@@ -0,0 +1,98 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// TODO(nate-thegrate): remove this file if @protected changes, or add a test if it doesn't.
+// https://github.com/dart-lang/sdk/issues/57094
+
+import 'package:analyzer/analysis_rule/analysis_rule.dart';
+import 'package:analyzer/analysis_rule/rule_context.dart';
+import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/error/error.dart';
+
+class ProtectPublicStateSubtypes extends AnalysisRule {
+ ProtectPublicStateSubtypes()
+ : super(
+ name: code.name,
+ description:
+ 'Public State subtypes should add @protected when overriding methods '
+ 'to avoid exposing internal logic to developers.',
+ );
+
+ static const LintCode code = LintCode(
+ 'protect_public_state_subtypes',
+ 'Public State subtypes should add @protected when overriding methods '
+ 'to avoid exposing internal logic to developers.',
+ severity: DiagnosticSeverity.ERROR,
+ );
+
+ @override
+ DiagnosticCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
+ final visitor = _Visitor(this, context);
+ registry.addClassDeclaration(this, visitor);
+ }
+}
+
+class _Visitor extends RecursiveAstVisitor<void> {
+ _Visitor(this.rule, this.context);
+
+ final AnalysisRule rule;
+ final RuleContext context;
+
+ /// Holds the `State` class [InterfaceElement].
+ InterfaceElement? _stateElement;
+
+ bool _isPublicStateSubtype(InterfaceElement element) {
+ if (!element.isPublic) {
+ return false;
+ }
+ final InterfaceElement? stateElement = _stateElement;
+ if (stateElement != null) {
+ return element.allSupertypes.any((InterfaceType t) => t.element == stateElement);
+ }
+ for (final InterfaceType superType in element.allSupertypes) {
+ if (superType.element.name == 'State') {
+ _stateElement = superType.element;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @override
+ void visitClassDeclaration(ClassDeclaration node) {
+ final InterfaceElement? element = node.declaredFragment?.element;
+ if (element != null && _isPublicStateSubtype(element)) {
+ node.visitChildren(this);
+ }
+ }
+
+ /// Checks whether overridden `State` methods have the `@protected` annotation,
+ /// and reports the method if not.
+ @override
+ void visitMethodDeclaration(MethodDeclaration node) {
+ switch (node.name.lexeme) {
+ case 'initState':
+ case 'setState':
+ case 'didUpdateWidget':
+ case 'didChangeDependencies':
+ case 'reassemble':
+ case 'deactivate':
+ case 'activate':
+ case 'dispose':
+ case 'build':
+ case 'debugFillProperties':
+ final ExecutableElement? element = node.declaredFragment?.element;
+ if (element != null && !element.metadata.hasProtected) {
+ rule.reportAtNode(node);
+ }
+ }
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/lib/src/rules/render_box_intrinsics.dart b/dev/flutter_analyzer_plugin/lib/src/rules/render_box_intrinsics.dart
new file mode 100644
index 0000000..4769c70
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/lib/src/rules/render_box_intrinsics.dart
@@ -0,0 +1,127 @@
+// Copyright 2014 The Flutter Authors. 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/analysis_rule/analysis_rule.dart';
+import 'package:analyzer/analysis_rule/rule_context.dart';
+import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element.dart';
+import 'package:analyzer/dart/element/type.dart';
+import 'package:analyzer/error/error.dart';
+
+/// Verify that no RenderBox subclasses call compute* instead of get* for
+/// computing the intrinsic dimensions. The [candidates] variable contains the
+/// full list of RenderBox intrinsic method invocations checked by this rule.
+const Map<String, String> candidates = <String, String>{
+ 'computeDryBaseline': 'getDryBaseline',
+ 'computeDryLayout': 'getDryLayout',
+ 'computeDistanceToActualBaseline': 'getDistanceToBaseline, or getDistanceToActualBaseline',
+ 'computeMaxIntrinsicHeight': 'getMaxIntrinsicHeight',
+ 'computeMinIntrinsicHeight': 'getMinIntrinsicHeight',
+ 'computeMaxIntrinsicWidth': 'getMaxIntrinsicWidth',
+ 'computeMinIntrinsicWidth': 'getMinIntrinsicWidth',
+};
+
+class RenderBoxIntrinsicCalculationRule extends AnalysisRule {
+ RenderBoxIntrinsicCalculationRule()
+ : super(
+ name: code.name,
+ description: 'get* methods should be used to obtain the intrinsics of a RenderBox.',
+ );
+
+ static const LintCode code = LintCode(
+ 'render_box_intrinsics',
+ 'Typically the get* methods should be used to obtain the intrinsics of a RenderBox.',
+ correctionMessage: 'Consider calling {0} instead.',
+ severity: DiagnosticSeverity.ERROR,
+ );
+
+ @override
+ LintCode get diagnosticCode => code;
+
+ @override
+ void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
+ final visitor = _Visitor(this, context);
+ registry.addSimpleIdentifier(this, visitor);
+ }
+}
+
+class _Visitor extends SimpleAstVisitor<void> {
+ _Visitor(this.rule, this.context);
+
+ final AnalysisRule rule;
+ final RuleContext context;
+
+ final Map<InterfaceElement, bool> _isRenderBoxClassElementCache = <InterfaceElement, bool>{};
+
+ // The cached version, call this method instead of _checkIfImplementsRenderBox.
+ bool _implementsRenderBox(InterfaceElement interfaceElement) {
+ // Framework naming convention: a RenderObject subclass names have "Render" in its name.
+ final String? name = interfaceElement.name;
+ if (name == null || !name.contains('Render')) {
+ return false;
+ }
+ return name == 'RenderBox' ||
+ _isRenderBoxClassElementCache.putIfAbsent(
+ interfaceElement,
+ () => _checkIfImplementsRenderBox(interfaceElement),
+ );
+ }
+
+ bool _checkIfImplementsRenderBox(InterfaceElement element) {
+ return element.allSupertypes.any(
+ (InterfaceType interface) => _implementsRenderBox(interface.element),
+ );
+ }
+
+ static bool _checkIfRenderBoxParent(AstNode? node) {
+ if (node == null) {
+ return false;
+ }
+ if (node case ClassDeclaration(:final ClassNamePart namePart)) {
+ // Ignore the RenderBox class implementation: that's the only place the
+ // compute* methods are supposed to be called.
+ return namePart.typeName.lexeme == 'RenderBox';
+ }
+ return _checkIfRenderBoxParent(node.parent);
+ }
+
+ static bool _checkForCommentContext(AstNode? node) {
+ if (node == null) {
+ return false;
+ }
+ if (node is CommentReference) {
+ return true;
+ }
+ return _checkForCommentContext(node.parent);
+ }
+
+ @override
+ void visitSimpleIdentifier(SimpleIdentifier node) {
+ if (node.parent is CommentReference) {
+ return;
+ }
+ final String? correctMethodName = candidates[node.name];
+ if (correctMethodName == null) {
+ return;
+ }
+ if (_checkIfRenderBoxParent(node.parent) || _checkForCommentContext(node.parent)) {
+ return;
+ }
+ final bool isCallingSuperImplementation = switch (node.parent) {
+ PropertyAccess(target: SuperExpression()) ||
+ MethodInvocation(target: SuperExpression()) => true,
+ _ => false,
+ };
+ if (isCallingSuperImplementation) {
+ return;
+ }
+ final Element? declaredInClassElement = node.element?.enclosingElement;
+ if (declaredInClassElement is InterfaceElement &&
+ _implementsRenderBox(declaredInClassElement)) {
+ rule.reportAtNode(node, arguments: <Object>[correctMethodName]);
+ }
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/pubspec.yaml b/dev/flutter_analyzer_plugin/pubspec.yaml
new file mode 100644
index 0000000..00535df
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/pubspec.yaml
@@ -0,0 +1,20 @@
+name: flutter_analyzer_plugin
+description: Custom analysis rules for flutter/flutter
+version: 0.0.1
+publish_to: none
+
+resolution: workspace
+
+environment:
+ sdk: ^3.7.0
+
+dependencies:
+ analysis_server_plugin: any
+ analyzer: any
+ path: any
+
+dev_dependencies:
+ analyzer_testing: any
+ test_reflective_loader: any
+
+# PUBSPEC CHECKSUM: jdpqac
diff --git a/dev/flutter_analyzer_plugin/test/avoid_future_catch_error_test.dart b/dev/flutter_analyzer_plugin/test/avoid_future_catch_error_test.dart
new file mode 100644
index 0000000..c558c07
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/avoid_future_catch_error_test.dart
@@ -0,0 +1,52 @@
+// Copyright 2014 The Flutter Authors. 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/lint/registry.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
+import 'package:flutter_analyzer_plugin/src/rules/avoid_future_catch_error.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+@reflectiveTest
+class AvoidFutureCatchErrorTest extends AnalysisRuleTest {
+ @override
+ void setUp() {
+ Registry.ruleRegistry.registerWarningRule(AvoidFutureCatchError());
+ super.setUp();
+ }
+
+ @override
+ String get analysisRule => AvoidFutureCatchError.code.name;
+
+ static const String source = '''
+import 'dart:async';
+
+// This extension isn't picked up from dart:async, so we just fake it.
+extension MyFutureExtension<T> on Future<T> {
+ Future<T> onError<E extends Object>(
+ FutureOr<T> handleError(E error, StackTrace stackTrace), {
+ bool test(E error)?,
+ }) {
+ return this;
+ }
+}
+
+void main() {
+ Future<void>.value().catchError((e, st) => null); // ERROR
+ Future<void>.value().onError((e, st) => null); // ERROR
+ Future<void>.value().then((_) => null, onError: (e, st) => null); // OK
+}
+''';
+
+ // ignore: non_constant_identifier_names
+ Future<void> test_avoid_future_catch_error() async {
+ await assertDiagnostics(source, <ExpectedDiagnostic>[lint(313, 48), lint(374, 45)]);
+ }
+}
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(AvoidFutureCatchErrorTest);
+ });
+}
diff --git a/dev/flutter_analyzer_plugin/test/no_double_clamp_test.dart b/dev/flutter_analyzer_plugin/test/no_double_clamp_test.dart
new file mode 100644
index 0000000..d49beee
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/no_double_clamp_test.dart
@@ -0,0 +1,71 @@
+// Copyright 2014 The Flutter Authors. 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/lint/registry.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
+import 'package:flutter_analyzer_plugin/src/rules/no_double_clamp.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+@reflectiveTest
+class NoDoubleClampTest extends AnalysisRuleTest {
+ @override
+ void setUp() {
+ Registry.ruleRegistry.registerWarningRule(NoDoubleClamp());
+ super.setUp();
+ }
+
+ @override
+ String get analysisRule => NoDoubleClamp.code.name;
+
+ static const String source = '''
+class ClassWithAClampMethod {
+ ClassWithAClampMethod clamp(double min, double max) => this;
+}
+
+void testNoDoubleClamp(int input) {
+ final ClassWithAClampMethod nonDoubleClamp = ClassWithAClampMethod();
+ // ignore: unnecessary_nullable_for_final_variable_declarations
+ final ClassWithAClampMethod? nonDoubleClamp2 = nonDoubleClamp;
+ // ignore: unnecessary_nullable_for_final_variable_declarations
+ final int? nullableInt = input;
+ final double? nullableDouble = nullableInt?.toDouble();
+
+ nonDoubleClamp.clamp(0, 2);
+ input.clamp(0, 2);
+ input.clamp(0.0, 2); // ERROR: input.clamp(0.0, 2)
+ input.toDouble().clamp(0, 2); // ERROR: input.toDouble().clamp(0, 2)
+
+ nonDoubleClamp2?.clamp(0, 2);
+ nullableInt?.clamp(0, 2);
+ nullableInt?.clamp(0, 2.0); // ERROR: nullableInt?.clamp(0, 2.0)
+ nullableDouble?.clamp(0, 2); // ERROR: nullableDouble?.clamp(0, 2)
+
+ // ignore: unused_local_variable
+ final ClassWithAClampMethod Function(double, double)? tearOff1 = nonDoubleClamp2?.clamp;
+ // ignore: unused_local_variable
+ final num Function(num, num)? tearOff2 = nullableInt?.clamp; // ERROR: nullableInt?.clamp
+ // ignore: unused_local_variable
+ final num Function(num, num)? tearOff3 = nullableDouble?.clamp; // ERROR: nullableDouble?.clamp
+}
+''';
+
+ // ignore: non_constant_identifier_names
+ Future<void> test_no_double_clamp() async {
+ await assertDiagnostics(source, <ExpectedDiagnostic>[
+ lint(553, 5),
+ lint(617, 5),
+ lint(745, 5),
+ lint(815, 5),
+ lint(1084, 5),
+ lint(1214, 5),
+ ]);
+ }
+}
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(NoDoubleClampTest);
+ });
+}
diff --git a/dev/flutter_analyzer_plugin/test/no_stopwatches_test.dart b/dev/flutter_analyzer_plugin/test/no_stopwatches_test.dart
new file mode 100644
index 0000000..01cf483
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/no_stopwatches_test.dart
@@ -0,0 +1,122 @@
+// Copyright 2014 The Flutter Authors. 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/lint/registry.dart';
+import 'package:analyzer/utilities/package_config_file_builder.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
+import 'package:flutter_analyzer_plugin/src/rules/no_stopwatches.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'package_mixins/external_stopwatches_mixin.dart';
+
+@reflectiveTest
+class NoStopwatchesTest extends AnalysisRuleTest with ExternalStopwatchesPackage {
+ @override
+ void setUp() {
+ Registry.ruleRegistry.registerWarningRule(NoStopwatches());
+ super.setUp();
+
+ writeTestPackageConfig(PackageConfigFileBuilder()..addExternalStopwatchesPackage(this));
+ }
+
+ @override
+ String get analysisRule => NoStopwatches.code.name;
+
+ static const String source = '''
+// Copyright 2014 The Flutter Authors. 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:external_stopwatches/external_stopwatches.dart' as externallib;
+
+typedef ExternalStopwatchConstructor = externallib.MyStopwatch Function();
+
+class StopwatchAtHome extends Stopwatch {
+ StopwatchAtHome();
+ StopwatchAtHome.create() : this();
+
+ Stopwatch get stopwatch => this;
+}
+
+void testNoStopwatches(Stopwatch stopwatch) {
+ // OK for now, but we probably want to catch public APIs that take a Stopwatch?
+ stopwatch.runtimeType;
+ // Bad: introducing Stopwatch from dart:core.
+ final Stopwatch localVariable = Stopwatch(); // ERROR: Stopwatch()
+ // Bad: introducing Stopwatch from dart:core.
+ Stopwatch().runtimeType; // ERROR: Stopwatch()
+
+ (localVariable..runtimeType) // OK: not directly introducing Stopwatch.
+ .runtimeType;
+
+ // Bad: introducing a Stopwatch subclass.
+ StopwatchAtHome().runtimeType; // ERROR: StopwatchAtHome()
+
+ // OK: not directly introducing Stopwatch.
+ Stopwatch anotherStopwatch = stopwatch;
+ // Bad: introducing a Stopwatch constructor.
+ StopwatchAtHome Function() constructor = StopwatchAtHome.new; // ERROR: StopwatchAtHome.new
+ assert(() {
+ anotherStopwatch = constructor()..runtimeType;
+ // Bad: introducing a Stopwatch constructor.
+ constructor = StopwatchAtHome.create; // ERROR: StopwatchAtHome.create
+ anotherStopwatch = constructor()..runtimeType;
+ return true;
+ }());
+ anotherStopwatch.runtimeType;
+
+ // Bad: introducing an external Stopwatch constructor.
+ externallib.MyStopwatch.create(); // ERROR: externallib.MyStopwatch.create()
+ ExternalStopwatchConstructor? externalConstructor;
+
+ assert(() {
+ // Bad: introducing an external Stopwatch constructor.
+ externalConstructor = externallib.MyStopwatch.new; // ERROR: externallib.MyStopwatch.new
+ return true;
+ }());
+ externalConstructor?.call();
+
+ // Bad: introducing an external Stopwatch.
+ externallib.stopwatch.runtimeType; // ERROR: externallib.stopwatch
+ // Bad: calling an external function that returns a Stopwatch.
+ externallib.createMyStopwatch().runtimeType; // ERROR: externallib.createMyStopwatch()
+ // Bad: calling an external function that returns a Stopwatch.
+ externallib.createStopwatch().runtimeType; // ERROR: externallib.createStopwatch()
+ // Bad: introducing the tear-off form of an external function that returns a Stopwatch.
+ externalConstructor = externallib.createMyStopwatch; // ERROR: externallib.createMyStopwatch
+
+ // OK: existing instance.
+ constructor.call().stopwatch;
+}
+
+void testStopwatchIgnore(Stopwatch stopwatch) {
+ Stopwatch().runtimeType; // flutter_ignore: stopwatch (see analyze.dart)
+ Stopwatch().runtimeType; // flutter_ignore: some_other_ignores, stopwatch (see analyze.dart)
+}
+''';
+
+ // ignore: unreachable_from_main, non_constant_identifier_names
+ Future<void> test_no_stopwatches() async {
+ await assertDiagnostics(source, <ExpectedDiagnostic>[
+ lint(696, 9),
+ lint(781, 9),
+ lint(970, 15),
+ lint(1207, 19),
+ lint(1390, 22),
+ lint(1615, 30),
+ lint(1845, 27),
+ lint(2028, 9),
+ lint(2162, 17),
+ lint(2316, 15),
+ lint(2513, 17),
+ ]);
+ }
+}
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(NoStopwatchesTest);
+ });
+}
diff --git a/dev/flutter_analyzer_plugin/test/package_mixins/external_stopwatches_mixin.dart b/dev/flutter_analyzer_plugin/test/package_mixins/external_stopwatches_mixin.dart
new file mode 100644
index 0000000..87374e4
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/package_mixins/external_stopwatches_mixin.dart
@@ -0,0 +1,70 @@
+// Copyright 2014 The Flutter Authors. 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/utilities/package_config_file_builder.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+
+extension ExternalStopwatchesExtension on PackageConfigFileBuilder {
+ PackageConfigFileBuilder addExternalStopwatchesPackage(AnalysisRuleTest test) {
+ add(
+ name: ExternalStopwatchesPackage._externalStopwatchesPackageName,
+ rootPath: test.convertPath(ExternalStopwatchesPackage._externalStopwatchesPackageRoot),
+ );
+ return this;
+ }
+}
+
+/// Mixin application that allows for `package:meta` imports in tests.
+mixin ExternalStopwatchesPackage on AnalysisRuleTest {
+ static const String _externalStopwatchesPackageName = 'external_stopwatches';
+ static const String _externalStopwatchesPackageRoot =
+ '/packages/$_externalStopwatchesPackageName';
+
+ @override
+ void setUp() {
+ super.setUp();
+ newFile('$_externalStopwatchesPackageRoot/lib/external_stopwatches.dart', '''
+// External Library that creates Stopwatches. This file will not be analyzed but
+// its symbols will be imported by tests.
+
+class MyStopwatch implements Stopwatch {
+ MyStopwatch();
+ MyStopwatch.create() : this();
+
+ @override
+ Duration get elapsed => throw UnimplementedError();
+
+ @override
+ int get elapsedMicroseconds => throw UnimplementedError();
+
+ @override
+ int get elapsedMilliseconds => throw UnimplementedError();
+
+ @override
+ int get elapsedTicks => throw UnimplementedError();
+
+ @override
+ int get frequency => throw UnimplementedError();
+
+ @override
+ bool get isRunning => throw UnimplementedError();
+
+ @override
+ void reset() {}
+
+ @override
+ void start() {}
+
+ @override
+ void stop() {}
+}
+
+final MyStopwatch stopwatch = MyStopwatch.create();
+
+MyStopwatch createMyStopwatch() => MyStopwatch();
+Stopwatch createStopwatch() => Stopwatch();
+
+''');
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/test/package_mixins/meta_mixin.dart b/dev/flutter_analyzer_plugin/test/package_mixins/meta_mixin.dart
new file mode 100644
index 0000000..8f322e0
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/package_mixins/meta_mixin.dart
@@ -0,0 +1,35 @@
+// Copyright 2014 The Flutter Authors. 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/utilities/package_config_file_builder.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+
+extension MetaPackageConfigExtension on PackageConfigFileBuilder {
+ PackageConfigFileBuilder addMetaPackage(AnalysisRuleTest test) {
+ add(
+ name: MetaPackage._metaPackageName,
+ rootPath: test.convertPath(MetaPackage._metaPackageRoot),
+ );
+ return this;
+ }
+}
+
+/// Mixin application that allows for `package:meta` imports in tests.
+mixin MetaPackage on AnalysisRuleTest {
+ static const String _metaPackageName = 'meta';
+ static const String _metaPackageRoot = '/packages/$_metaPackageName';
+
+ @override
+ void setUp() {
+ super.setUp();
+ newFile('$_metaPackageRoot/lib/meta.dart', '''
+library meta;
+
+const protected = Object();
+const mustCallSuper = Object();
+const factory = Object();
+const optionalTypeArgs = Object();
+''');
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/test/package_mixins/widgets_mixin.dart b/dev/flutter_analyzer_plugin/test/package_mixins/widgets_mixin.dart
new file mode 100644
index 0000000..ba14474
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/package_mixins/widgets_mixin.dart
@@ -0,0 +1,95 @@
+// Copyright 2014 The Flutter Authors. 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/utilities/package_config_file_builder.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+
+extension FlutterWidgetsPackageConfigExtension on PackageConfigFileBuilder {
+ PackageConfigFileBuilder addFlutterWidgetsPackage(AnalysisRuleTest test) {
+ add(
+ name: FlutterWidgetsPackage._flutterPackageName,
+ rootPath: test.convertPath(FlutterWidgetsPackage._flutterPackageRoot),
+ );
+ return this;
+ }
+}
+
+/// Mixin application that allows for `package:flutter/widgets.dart` imports in tests.
+mixin FlutterWidgetsPackage on AnalysisRuleTest {
+ static const String _flutterPackageName = 'flutter';
+ static const String _flutterPackageRoot = '/packages/$_flutterPackageName';
+
+ @override
+ void setUp() {
+ super.setUp();
+ newFile('$_flutterPackageRoot/lib/widgets.dart', '''
+library widgets;
+
+abstract class StatefulWidget {
+ const StatefulWidget();
+
+ @protected
+ @factory
+ State createState();
+}
+
+mixin Diagnosticable {
+ @protected
+ @mustCallSuper
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {}
+}
+
+class DiagnosticPropertiesBuilder {}
+class BuildContext {}
+class Widget {}
+
+typedef VoidCallback = void Function();
+
+@optionalTypeArgs
+abstract class State<T extends StatefulWidget> with Diagnosticable {
+ @protected
+ @mustCallSuper
+ void initState() {}
+
+ @mustCallSuper
+ @protected
+ void didUpdateWidget(covariant T oldWidget) {}
+
+ @protected
+ @mustCallSuper
+ void reassemble() {}
+
+ @protected
+ void setState(VoidCallback fn) {}
+
+ @protected
+ @mustCallSuper
+ void deactivate() {}
+
+ @protected
+ @mustCallSuper
+ void activate() {}
+
+ @protected
+ @mustCallSuper
+ void dispose() {}
+
+ @protected
+ Widget build(BuildContext context);
+
+ @protected
+ @mustCallSuper
+ void didChangeDependencies() {}
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ }
+
+ // If @protected State methods are added or removed, the analysis rule should be
+ // updated accordingly (dev/flutter_analyzer_plugin/lib/src/rules/protect_public_state_subtypes.dart)
+}
+''');
+ }
+}
diff --git a/dev/flutter_analyzer_plugin/test/protect_public_state_subtypes_test.dart b/dev/flutter_analyzer_plugin/test/protect_public_state_subtypes_test.dart
new file mode 100644
index 0000000..60eeec4
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/protect_public_state_subtypes_test.dart
@@ -0,0 +1,171 @@
+// Copyright 2014 The Flutter Authors. 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/lint/registry.dart';
+import 'package:analyzer/utilities/package_config_file_builder.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
+import 'package:flutter_analyzer_plugin/src/rules/protect_public_state_subtypes.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import 'package_mixins/meta_mixin.dart';
+import 'package_mixins/widgets_mixin.dart';
+
+@reflectiveTest
+class ProtectPublicStateSubtypesTest extends AnalysisRuleTest
+ with MetaPackage, FlutterWidgetsPackage {
+ @override
+ void setUp() {
+ Registry.ruleRegistry.registerWarningRule(ProtectPublicStateSubtypes());
+ super.setUp();
+
+ writeTestPackageConfig(
+ PackageConfigFileBuilder()
+ ..addFlutterWidgetsPackage(this)
+ ..addMetaPackage(this),
+ );
+ }
+
+ @override
+ String get analysisRule => ProtectPublicStateSubtypes.code.name;
+
+ static const String source = '''
+import 'package:flutter/widgets.dart';
+import 'package:meta/meta.dart';
+
+class MyWidget extends StatefulWidget {
+
+ @override
+ State createState() => MyWidgetStateBad();
+}
+
+class MyWidgetStateBad extends State<MyWidget>{
+ @override
+ void initState() { // ERROR
+ super.initState();
+ }
+
+ @override
+ void didUpdateWidget(covariant MyWidget oldWidget) { // ERROR
+ super.didUpdateWidget(oldWidget);
+ }
+
+ @override
+ void reassemble() { // ERROR
+ super.reassemble();
+ }
+
+ @override
+ void setState(VoidCallback fn) {} // ERROR
+
+ @override
+ void deactivate() { // ERROR
+ super.deactivate();
+ }
+
+ @override
+ void activate() { // ERROR
+ super.activate();
+ }
+
+ @override
+ void dispose() { // ERROR
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => Widget();
+
+ @override
+ void didChangeDependencies() { // ERROR
+ super.didChangeDependencies();
+ }
+
+ @override
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) { // ERROR
+ super.debugFillProperties(properties);
+ }
+}
+
+class MyWidgetStateValid extends State<MyWidget>{
+ @override
+ @protected
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ @protected
+ void didUpdateWidget(covariant MyWidget oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ }
+
+ @override
+ @protected
+ void reassemble() {
+ super.reassemble();
+ }
+
+ @override
+ @protected
+ void setState(VoidCallback fn) {}
+
+ @override
+ @protected
+ void deactivate() {
+ super.deactivate();
+ }
+
+ @override
+ @protected
+ void activate() {
+ super.activate();
+ }
+
+ @override
+ @protected
+ void dispose() {
+ super.dispose();
+ }
+
+ @override
+ @protected
+ Widget build(BuildContext context) => Widget();
+
+ @override
+ @protected
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ }
+
+ @override
+ @protected
+ void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+ super.debugFillProperties(properties);
+ }
+}
+''';
+
+ // ignore: non_constant_identifier_names
+ Future<void> test_protect_public_state_subtypes() async {
+ await assertDiagnostics(source, <ExpectedDiagnostic>[
+ lint(224, 66),
+ lint(294, 115),
+ lint(413, 68),
+ lint(485, 45),
+ lint(543, 68),
+ lint(615, 64),
+ lint(683, 62),
+ lint(749, 59),
+ lint(812, 90),
+ lint(906, 134),
+ ]);
+ }
+}
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(ProtectPublicStateSubtypesTest);
+ });
+}
diff --git a/dev/flutter_analyzer_plugin/test/render_box_intrinsics_test.dart b/dev/flutter_analyzer_plugin/test/render_box_intrinsics_test.dart
new file mode 100644
index 0000000..b9a7b82
--- /dev/null
+++ b/dev/flutter_analyzer_plugin/test/render_box_intrinsics_test.dart
@@ -0,0 +1,104 @@
+// Copyright 2014 The Flutter Authors. 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/lint/registry.dart';
+import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
+import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
+import 'package:flutter_analyzer_plugin/src/rules/render_box_intrinsics.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+@reflectiveTest
+class RenderBoxIntrinsicCalculationRuleTest extends AnalysisRuleTest {
+ @override
+ void setUp() {
+ Registry.ruleRegistry.registerWarningRule(RenderBoxIntrinsicCalculationRule());
+ super.setUp();
+ }
+
+ @override
+ String get analysisRule => RenderBoxIntrinsicCalculationRule.code.name;
+
+ static const String source = '''
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+abstract class RenderBox {
+ void computeDryBaseline() {}
+ void computeDryLayout() {}
+ void computeDistanceToActualBaseline() {}
+ void computeMaxIntrinsicHeight() {}
+ void computeMinIntrinsicHeight() {}
+ void computeMaxIntrinsicWidth() {}
+ void computeMinIntrinsicWidth() {}
+}
+
+mixin ARenderBoxMixin on RenderBox {
+ @override
+ void computeMaxIntrinsicWidth() {}
+
+ @override
+ void computeMinIntrinsicWidth() => computeMaxIntrinsicWidth(); // ERROR: computeMaxIntrinsicWidth(). Consider calling getMaxIntrinsicWidth instead.
+
+ @override
+ void computeMinIntrinsicHeight() {
+ final void Function() f =
+ computeMaxIntrinsicWidth; // ERROR: f = computeMaxIntrinsicWidth. Consider calling getMaxIntrinsicWidth instead.
+ f();
+ }
+}
+
+extension ARenderBoxExtension on RenderBox {
+ void test() {
+ computeDryBaseline(); // ERROR: computeDryBaseline(). Consider calling getDryBaseline instead.
+ computeDryLayout(); // ERROR: computeDryLayout(). Consider calling getDryLayout instead.
+ }
+}
+
+class RenderBoxSubclass1 extends RenderBox {
+ @override
+ void computeDryLayout() {
+ computeDistanceToActualBaseline(); // ERROR: computeDistanceToActualBaseline(). Consider calling getDistanceToBaseline, or getDistanceToActualBaseline instead.
+ }
+
+ @override
+ void computeDistanceToActualBaseline() {
+ computeMaxIntrinsicHeight(); // ERROR: computeMaxIntrinsicHeight(). Consider calling getMaxIntrinsicHeight instead.
+ }
+
+ /// [RenderBox.computeDryLayout]: // OK
+ double? getDryBaseline() {
+ return 0;
+ }
+}
+
+class RenderBoxSubclass2 extends RenderBox with ARenderBoxMixin {
+ @override
+ void computeMaxIntrinsicWidth() {
+ super.computeMinIntrinsicHeight(); // OK
+ super.computeMaxIntrinsicWidth(); // OK
+ final void Function() f = super.computeDryBaseline; // OK
+ f();
+ }
+}
+''';
+
+ // ignore: non_constant_identifier_names
+ Future<void> test_render_box_intrinsics() async {
+ await assertDiagnostics(source, <ExpectedDiagnostic>[
+ lint(585, 24),
+ lint(786, 24),
+ lint(980, 18),
+ lint(1079, 16),
+ lint(1264, 31),
+ lint(1488, 25),
+ ]);
+ }
+}
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(RenderBoxIntrinsicCalculationRuleTest);
+ });
+}
diff --git a/packages/flutter/lib/analysis_options.yaml b/packages/flutter/lib/analysis_options.yaml
index 60b5baf..8ab79bb 100644
--- a/packages/flutter/lib/analysis_options.yaml
+++ b/packages/flutter/lib/analysis_options.yaml
@@ -1,5 +1,14 @@
include: ../analysis_options.yaml
+plugins:
+ flutter_analyzer_plugin:
+ path: ../../dev/flutter_analyzer_plugin
+ diagnostics:
+ no_double_clamp: true
+ no_stopwatches: true
+ protect_public_state_subtypes: true
+ render_box_intrinsics: true
+
linter:
rules:
# diagnostic_describe_all_properties: true # blocked on https://github.com/dart-lang/sdk/issues/47418
diff --git a/packages/flutter/test/analysis_options.yaml b/packages/flutter/test/analysis_options.yaml
index 594830c..1c12990 100644
--- a/packages/flutter/test/analysis_options.yaml
+++ b/packages/flutter/test/analysis_options.yaml
@@ -1,5 +1,11 @@
include: ../analysis_options.yaml
+plugins:
+ flutter_analyzer_plugin:
+ path: ../../dev/flutter_analyzer_plugin
+ diagnostics:
+ no_stopwatches: true
+
linter:
rules:
# Tests try to throw and catch things in exciting ways all the time, so
diff --git a/packages/flutter_tools/analysis_options.yaml b/packages/flutter_tools/analysis_options.yaml
index 71e9519..e8af77b 100644
--- a/packages/flutter_tools/analysis_options.yaml
+++ b/packages/flutter_tools/analysis_options.yaml
@@ -9,6 +9,12 @@
- linux/**
include: ../analysis_options.yaml
+plugins:
+ flutter_analyzer_plugin:
+ path: ../../dev/flutter_analyzer_plugin
+ diagnostics:
+ avoid_future_catch_error: true
+
linter:
rules:
avoid_catches_without_on_clauses: true
diff --git a/pubspec.lock b/pubspec.lock
index 2cf2ac6..2162382 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -25,6 +25,14 @@
url: "https://pub.dev"
source: hosted
version: "0.1.7"
+ analysis_server_plugin:
+ dependency: transitive
+ description:
+ name: analysis_server_plugin
+ sha256: "63a41a9c0e12574c39f0207a193f8bdb65c78aa1d791c98f0aee6a91c4387df4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.9"
analyzer:
dependency: "direct main"
description:
@@ -33,6 +41,22 @@
url: "https://pub.dev"
source: hosted
version: "10.1.0"
+ analyzer_plugin:
+ dependency: transitive
+ description:
+ name: analyzer_plugin
+ sha256: "383e9d91be2ae8ab4a423656f1b39152d2ac5aaa88051d4bc178f89d071a90e0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.14.3"
+ analyzer_testing:
+ dependency: transitive
+ description:
+ name: analyzer_testing
+ sha256: "2cdc399ca5110d9059b0ef3c73a25d8d5357318d9057bf68e5358a013b5e9c4d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
animations:
dependency: "direct main"
description:
@@ -1124,6 +1148,14 @@
url: "https://pub.dev"
source: hosted
version: "0.6.18"
+ test_reflective_loader:
+ dependency: transitive
+ description:
+ name: test_reflective_loader
+ sha256: d828d5ca15179aaac2aaf8f510cf0a52ec28e0031681b044ec5e581a4b8002e7
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.0"
typed_data:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index dccaf20..b286341 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -5,6 +5,7 @@
workspace:
- dev/a11y_assessments
+ - dev/flutter_analyzer_plugin
- dev/automated_tests
- dev/benchmarks/complex_layout
- dev/benchmarks/imitation_game_flutter