[ Analyzer ] Add diagnostics for Flutter Widget Previews
The Flutter Widget Previewer relies on code generation to import code from
a developer's project into a generated 'widget preview scaffold' that lives
in the project's '.dart_tool' directory. The nature of this implementation
means that any symbols referenced in the declaration of the preview (e.g.,
in the invocation of the `Preview(...)` constructor or the name of the
preview function) must be publicly accessible from outside the library in
which they are defined. One of the main uses for this diagnostic is to flag
usages of private symbols within preview declarations.
These diagnostics also ensures that the `@Preview(...)` annotation can only be
applied to a functions or constructors that:
- Are statically accessible (e.g., no instance methods)
- Have explicit implementations (e.g., not abstract or external)
Fixes https://github.com/flutter/flutter/issues/167193
Change-Id: I208c7f18c8d4ad826fed46a0dc625d090ff186ef
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/423860
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Paul Berry <paulberry@google.com>
Auto-Submit: Ben Konyi <bkonyi@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
index d1fc26a..de8c6cb 100644
--- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
+++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml
@@ -3635,6 +3635,10 @@
status: noFix
WarningCode.INVALID_USE_OF_VISIBLE_FOR_TESTING_MEMBER:
status: noFix
+WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION:
+ status: needsEvaluation
+WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT:
+ status: needsEvaluation
WarningCode.INVALID_VISIBILITY_ANNOTATION:
status: hasFix
WarningCode.INVALID_VISIBLE_FOR_OVERRIDING_ANNOTATION:
diff --git a/pkg/analyzer/lib/src/error/best_practices_verifier.dart b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
index 5a357ea..fc5dd22 100644
--- a/pkg/analyzer/lib/src/error/best_practices_verifier.dart
+++ b/pkg/analyzer/lib/src/error/best_practices_verifier.dart
@@ -33,6 +33,7 @@
import 'package:analyzer/src/error/error_handler_verifier.dart';
import 'package:analyzer/src/error/must_call_super_verifier.dart';
import 'package:analyzer/src/error/null_safe_api_verifier.dart';
+import 'package:analyzer/src/error/widget_preview_verifier.dart';
import 'package:analyzer/src/lint/constants.dart';
import 'package:analyzer/src/utilities/extensions/ast.dart';
import 'package:analyzer/src/utilities/extensions/element.dart';
@@ -81,6 +82,8 @@
_errorReporter,
);
+ final WidgetPreviewVerifier _widgetPreviewVerifier;
+
/// The [WorkspacePackage] in which [_currentLibrary] is declared.
final WorkspacePackage? _workspacePackage;
@@ -129,6 +132,7 @@
),
_mustCallSuperVerifier = MustCallSuperVerifier(_errorReporter),
_nullSafeApiVerifier = NullSafeApiVerifier(_errorReporter, typeSystem),
+ _widgetPreviewVerifier = WidgetPreviewVerifier(_errorReporter),
_workspacePackage = workspacePackage {
_deprecatedVerifier.pushInDeprecatedValue(_currentLibrary.hasDeprecated);
_inDoNotStoreMember = _currentLibrary.metadata2.hasDoNotStore;
@@ -137,6 +141,7 @@
@override
void visitAnnotation(Annotation node) {
_annotationVerifier.checkAnnotation(node);
+ _widgetPreviewVerifier.checkAnnotation(node);
super.visitAnnotation(node);
}
@@ -1689,6 +1694,7 @@
if (element == null) {
return;
}
+
_checkForInvalidDoNotSubmitAccess(identifier, element);
if (_inCurrentLibrary(element)) {
diff --git a/pkg/analyzer/lib/src/error/codes.g.dart b/pkg/analyzer/lib/src/error/codes.g.dart
index 51d6495..9fa6722f 100644
--- a/pkg/analyzer/lib/src/error/codes.g.dart
+++ b/pkg/analyzer/lib/src/error/codes.g.dart
@@ -6965,6 +6965,23 @@
hasPublishedDocs: true,
);
+ static const WarningCode INVALID_WIDGET_PREVIEW_APPLICATION = WarningCode(
+ 'INVALID_WIDGET_PREVIEW_APPLICATION',
+ "The '@Preview(...)' annotation can only be applied to public, statically "
+ "accessible constructors and functions.",
+ );
+
+ /// Parameters:
+ /// 0: the name of the private symbol
+ /// 1: the name of the proposed public symbol equivalent
+ static const WarningCode
+ INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT = WarningCode(
+ 'INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT',
+ "'@Preview(...)' can only accept arguments that consist of literals and "
+ "public symbols.",
+ correctionMessage: "Rename private symbol '{0}' to '{1}'.",
+ );
+
/// Parameters:
/// 0: the name of the member
static const WarningCode MISSING_OVERRIDE_OF_MUST_BE_OVERRIDDEN_ONE =
diff --git a/pkg/analyzer/lib/src/error/error_code_values.g.dart b/pkg/analyzer/lib/src/error/error_code_values.g.dart
index 35a6dab..2122f72 100644
--- a/pkg/analyzer/lib/src/error/error_code_values.g.dart
+++ b/pkg/analyzer/lib/src/error/error_code_values.g.dart
@@ -1046,6 +1046,8 @@
WarningCode.INVALID_VISIBILITY_ANNOTATION,
WarningCode.INVALID_VISIBLE_FOR_OVERRIDING_ANNOTATION,
WarningCode.INVALID_VISIBLE_OUTSIDE_TEMPLATE_ANNOTATION,
+ WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION,
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
WarningCode.MISSING_OVERRIDE_OF_MUST_BE_OVERRIDDEN_ONE,
WarningCode.MISSING_OVERRIDE_OF_MUST_BE_OVERRIDDEN_THREE_PLUS,
WarningCode.MISSING_OVERRIDE_OF_MUST_BE_OVERRIDDEN_TWO,
diff --git a/pkg/analyzer/lib/src/error/widget_preview_verifier.dart b/pkg/analyzer/lib/src/error/widget_preview_verifier.dart
new file mode 100644
index 0000000..28fb34f
--- /dev/null
+++ b/pkg/analyzer/lib/src/error/widget_preview_verifier.dart
@@ -0,0 +1,241 @@
+// Copyright (c) 2025, 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/dart/ast/token.dart';
+import 'package:analyzer/dart/ast/visitor.dart';
+import 'package:analyzer/dart/element/element2.dart';
+import 'package:analyzer/error/listener.dart';
+import 'package:analyzer/src/dart/ast/ast.dart';
+import 'package:analyzer/src/error/codes.dart';
+import 'package:analyzer/src/utilities/extensions/flutter.dart';
+
+/// Helper for verifying the validity of @Preview(...) applications.
+///
+/// The Flutter Widget Previewer relies on code generation to import code from
+/// a developer's project into a generated 'widget preview scaffold' that lives
+/// in the project's '.dart_tool' directory. The nature of this implementation
+/// means that any symbols referenced in the declaration of the preview (e.g.,
+/// in the invocation of the `Preview(...)` constructor or the name of the
+/// preview function) must be publicly accessible from outside the library in
+/// which they are defined. One of the main uses for this verifier is to flag
+/// usages of private symbols within preview declarations.
+///
+/// This verifier also ensures that the `@Preview(...)` annotation can only be
+/// applied to a functions or constructors that:
+///
+/// - Are statically accessible (e.g., no instance methods)
+/// - Have explicit implementations (e.g., not abstract or external)
+class WidgetPreviewVerifier {
+ final ErrorReporter _errorReporter;
+
+ WidgetPreviewVerifier(this._errorReporter);
+
+ /// Check is [node] is a Widget Preview application and verify its
+ /// correctness.
+ void checkAnnotation(Annotation node) {
+ if (node.elementAnnotation?.isWidgetPreview ?? false) {
+ _checkWidgetPreview(node);
+ }
+ }
+
+ void _checkWidgetPreview(Annotation node) {
+ if (node.arguments == null) {
+ // This is an invalid annotation application since there's no constructor
+ // invocation.
+ return;
+ }
+
+ var parent = node.parent;
+ bool isValidApplication = switch (parent) {
+ // First, check that the preview application is happening in a supported
+ // context.
+ _ when !_isSupportedParent(node: parent) => false,
+ ConstructorDeclaration() => _isValidConstructorPreviewApplication(
+ declaration: parent,
+ ),
+ FunctionDeclaration() => _isValidFunctionPreviewApplication(
+ declaration: parent,
+ ),
+ MethodDeclaration() => _isValidMethodPreviewApplication(
+ declaration: parent,
+ ),
+ _ => false,
+ };
+
+ if (!isValidApplication) {
+ _errorReporter.atNode(
+ node.name,
+ WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION,
+ );
+ }
+
+ var visitor = _InvalidWidgetPreviewArgumentDetectorVisitor(
+ errorReporter: _errorReporter,
+ );
+ node.arguments!.accept(visitor);
+ }
+
+ bool _hasRequiredParameters(NodeList<FormalParameter> parameters) {
+ return parameters.any((e) => e.isRequired);
+ }
+
+ /// Returns true if `name` is private or `node.parent` is a [ClassDeclaration]
+ /// that has a private name.
+ bool _isPrivateContext({required Token? name, required AstNode node}) {
+ if (Identifier.isPrivateName(name?.lexeme ?? '')) {
+ return true;
+ }
+ var parent = node.parent;
+ if (parent == null) return false;
+
+ return switch (parent) {
+ ClassDeclaration(:var name) => Identifier.isPrivateName(name.lexeme),
+ EnumDeclaration(:var name) => Identifier.isPrivateName(name.lexeme),
+ ExtensionDeclaration(:var name) => Identifier.isPrivateName(
+ name?.lexeme ?? '',
+ ),
+ ExtensionTypeDeclaration(:var name) => Identifier.isPrivateName(
+ name.lexeme,
+ ),
+ MixinDeclaration(:var name) => Identifier.isPrivateName(name.lexeme),
+ _ => false,
+ };
+ }
+
+ /// Returns true if `node.parent` is a supported context for defining widget
+ /// previews.
+ ///
+ /// Currently, this only includes previews defined within classes and at the
+ /// top level of a compilation unit.
+ bool _isSupportedParent({required AstNode node}) {
+ return switch (node.parent) {
+ ClassDeclaration() || CompilationUnit() => true,
+ _ => false,
+ };
+ }
+
+ /// Returns true if `declaration` is a valid constructor target for a widget
+ /// preview application.
+ ///
+ /// Constructor preview applications are valid if:
+ /// - The class and constructor names are public
+ /// - The class is a subtype of Widget
+ /// - The class is not abstract or is a valid factory constructor
+ /// - The constructor is not external
+ /// - The constructor does not have any required arguments
+ bool _isValidConstructorPreviewApplication({
+ required ConstructorDeclaration declaration,
+ }) {
+ if (declaration case ConstructorDeclaration(
+ :var name,
+ :var externalKeyword,
+ :var factoryKeyword,
+ parent: ClassDeclaration(declaredFragment: ClassFragment(:var element)),
+ parameters: FormalParameterList(:var parameters),
+ )) {
+ return !_isPrivateContext(name: name, node: declaration) &&
+ element.isWidget &&
+ !(element.isAbstract && factoryKeyword == null) &&
+ externalKeyword == null &&
+ !_hasRequiredParameters(parameters);
+ }
+ return false;
+ }
+
+ /// Returns true if `declaration` is a valid top-level function target for a
+ /// widget preview application.
+ ///
+ /// Function preview applications are valid if:
+ /// - The function name is public
+ /// - The function is not a nested function
+ /// - The function is not external
+ /// - The function returns a subtype of `Widget` or `WidgetBuilder`
+ /// - The function does not have any required arguments
+ bool _isValidFunctionPreviewApplication({
+ required FunctionDeclaration declaration,
+ }) {
+ if (declaration case FunctionDeclaration(
+ :var name,
+ :var externalKeyword,
+ :NamedType returnType,
+ functionExpression: FunctionExpression(
+ parameters: FormalParameterList(:var parameters),
+ ),
+ )) {
+ return !_isPrivateContext(name: name, node: declaration) &&
+ // Check for nested function.
+ declaration.parent is! FunctionDeclarationStatement &&
+ externalKeyword == null &&
+ returnType.isValidWidgetPreviewReturnType &&
+ !_hasRequiredParameters(parameters);
+ }
+ return false;
+ }
+
+ /// Returns true if `declaration` is a valid static class member target for a
+ /// widget preview application.
+ ///
+ /// Class member preview applications are valid if:
+ /// - The function is static
+ /// - The function name is public
+ /// - The function is not external
+ /// - The function returns a subtype of `Widget` or `WidgetBuilder`
+ /// - The function does not have any required arguments
+ bool _isValidMethodPreviewApplication({
+ required MethodDeclaration declaration,
+ }) {
+ if (declaration case MethodDeclaration(
+ :var isStatic,
+ :var externalKeyword,
+ :var name,
+ :NamedType returnType,
+ parameters: FormalParameterList(:var parameters),
+ )) {
+ return !_isPrivateContext(name: name, node: declaration) &&
+ isStatic &&
+ // Check for nested function.
+ declaration.parent is! FunctionDeclarationStatement &&
+ externalKeyword == null &&
+ returnType.isValidWidgetPreviewReturnType &&
+ !_hasRequiredParameters(parameters);
+ }
+ return false;
+ }
+}
+
+class _InvalidWidgetPreviewArgumentDetectorVisitor extends RecursiveAstVisitor {
+ final ErrorReporter errorReporter;
+
+ NamedExpression? rootArgument;
+ _InvalidWidgetPreviewArgumentDetectorVisitor({required this.errorReporter});
+
+ @override
+ void visitArgumentList(ArgumentList node) {
+ for (var argument in node.arguments) {
+ // All arguments to Preview(...) are named.
+ if (argument is NamedExpression) {
+ rootArgument = argument;
+ visitNamedExpression(argument);
+ rootArgument = null;
+ }
+ }
+ }
+
+ @override
+ void visitNamedExpression(NamedExpression node) {
+ super.visitNamedExpression(node);
+ }
+
+ @override
+ void visitSimpleIdentifier(SimpleIdentifier node) {
+ if (Identifier.isPrivateName(node.name)) {
+ errorReporter.atNode(
+ rootArgument!,
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ arguments: [node.name, node.name.replaceFirst(RegExp('_*'), '')],
+ );
+ }
+ super.visitSimpleIdentifier(node);
+ }
+}
diff --git a/pkg/analyzer/lib/src/utilities/extensions/flutter.dart b/pkg/analyzer/lib/src/utilities/extensions/flutter.dart
index 404fbad..03b0881 100644
--- a/pkg/analyzer/lib/src/utilities/extensions/flutter.dart
+++ b/pkg/analyzer/lib/src/utilities/extensions/flutter.dart
@@ -4,12 +4,14 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element2.dart';
+import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/src/utilities/extensions/string.dart';
import 'package:collection/collection.dart';
const String widgetsUri = 'package:flutter/widgets.dart';
const _nameAlign = 'Align';
+const _nameBuildContext = 'BuildContext';
const _nameBuilder = 'Builder';
const _nameCenter = 'Center';
const _nameContainer = 'Container';
@@ -169,6 +171,14 @@
}
extension DartTypeExtension on DartType? {
+ /// Whether this is the Flutter type `BuildContext`.
+ bool get isBuildContext {
+ var self = this;
+ return self is InterfaceType &&
+ self.nullabilitySuffix == NullabilitySuffix.none &&
+ self.element3._isExactly(_nameBuildContext, _uriFramework);
+ }
+
/// Whether this is the 'dart.ui' class `Color`, or a subtype.
bool get isColor {
var self = this;
@@ -296,6 +306,16 @@
);
}
+ /// Whether this is a function type matching the Flutter typedef
+ /// `WidgetBuilder` (i.e., `Widget Function(BuildContext context)`).
+ bool get isWidgetBuilder {
+ var self = this;
+ return self is FunctionType &&
+ self.returnType.isWidgetType &&
+ self.formalParameters.length == 1 &&
+ self.formalParameters[0].type.isBuildContext;
+ }
+
/// Whether this is the Flutter class `Widget`, or its subtype.
bool get isWidgetType {
var self = this;
@@ -303,6 +323,23 @@
}
}
+extension ElementAnnotationExtension on ElementAnnotation {
+ static final Uri _flutterWidgetPreviewLibraryUri = Uri.parse(
+ 'package:flutter/src/widget_previews/widget_previews.dart',
+ );
+
+ /// Whether the annotation marks the associated member as being a widget
+ /// preview.
+ bool get isWidgetPreview {
+ var element2 = this.element2;
+ if (element2 is! ConstructorElement2) {
+ return false;
+ }
+ return element2.enclosingElement2.name3 == 'Preview' &&
+ element2.library2.uri == _flutterWidgetPreviewLibraryUri;
+ }
+}
+
extension ExpressionExtension on Expression {
/// Whether this is the `builder` argument.
bool get isBuilderArgument {
@@ -483,3 +520,16 @@
self.firstFragment.libraryFragment.source.uri == uri;
}
}
+
+extension NamedTypeExtension on NamedType {
+ /// Whether this type is a valid return type for a function annotated with
+ /// `@Preview(...)`.
+ ///
+ /// Valid widget preview return types are:
+ /// - `Widget`
+ /// - `Widget Function(BuildContext)` (aka `WidgetBuilder`)
+ bool get isValidWidgetPreviewReturnType {
+ var self = this;
+ return self.type.isWidgetType || self.type.isWidgetBuilder;
+ }
+}
diff --git a/pkg/analyzer/messages.yaml b/pkg/analyzer/messages.yaml
index 3d982f4..5d44ed2 100644
--- a/pkg/analyzer/messages.yaml
+++ b/pkg/analyzer/messages.yaml
@@ -25195,6 +25195,187 @@
void m() {}
}
```
+ INVALID_WIDGET_PREVIEW_APPLICATION:
+ problemMessage: "The '@Preview(...)' annotation can only be applied to public, statically accessible constructors and functions."
+ documentation: |-
+ #### Description
+
+ The analyzer produces this diagnostic when a `@Preview(...)` annotation
+ is applied to an invalid widget preview target. Widget previews can only
+ be applied to public, statically accessible, explicitly defined
+ constructors and functions.
+
+ #### Examples
+
+ The following code produces this diagnostic because `_myPrivatePreview`
+ is private:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ // Invalid application to private top-level function.
+ @[!Preview!]()
+ // ignore: unused_element
+ Widget _myPrivatePreview() => Text('Foo');
+ ```
+
+ The following code produces this diagnostic because `myExternalPreview`
+ is `external`:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+
+ // Invalid application to an external function.
+ @[!Preview!]()
+ external Widget myExternalPreview();
+ ```
+
+ The following code produces this diagnostic because `PublicWidget._()` is
+ private:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ class PublicWidget extends StatelessWidget {
+ // Invalid application to a private constructor.
+ @[!Preview!]()
+ PublicWidget._();
+
+ @override
+ Widget build(BuildContext) => Text('Foo');
+ }
+ ```
+
+ The following code produces this diagnostic because `instancePreview` is
+ an instance method:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ class PublicWidget extends StatelessWidget {
+ // Invalid application to a instance member.
+ @[!Preview!]()
+ Widget instancePreview() => PublicWidget();
+
+ @override
+ Widget build(BuildContext context) => Text('Foo');
+ }
+ ```
+
+ The following code produces this diagnostic because `_PrivateWidget` is
+ private:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ // ignore: unused_element
+ class _PrivateWidget extends StatelessWidget {
+ // Invalid application to a constructor of a private class.
+ @[!Preview!]()
+ _PrivateWidget();
+
+ @override
+ Widget build(BuildContext context) => Text('Foo');
+ }
+ ```
+
+ The following code produces this diagnostic because `_PrivateWidget` is
+ private:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ // ignore: unused_element
+ class _PrivateWidget extends StatelessWidget {
+ // Invalid application to a static method of a private class.
+ @[!Preview!]()
+ Widget privateStatic() => _PrivateWidget();
+
+ @override
+ Widget build(BuildContext context) => Text('Foo');
+ }
+ ```
+
+ The following code produces this diagnostic because `AbstractWidget` is
+ an `abstract` class:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ abstract class AbstractWidget extends StatelessWidget {
+ // Invalid application to a constructor of an abstract class.
+ @[!Preview!]()
+ AbstractWidget();
+
+ @override
+ Widget build(BuildContext context) => Text('Foo');
+ }
+ ```
+
+ #### Common fixes
+
+ Create a dedicated public, statically accessible, and explicitly defined
+ constructor, top-level function, or class member for use as a preview:
+
+ INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT:
+ problemMessage: "'@Preview(...)' can only accept arguments that consist of literals and public symbols."
+ correctionMessage: "Rename private symbol '{0}' to '{1}'."
+ comment: |-
+ Parameters:
+ 0: the name of the private symbol
+ 1: the name of the proposed public symbol equivalent
+ documentation: |-
+ #### Description
+
+ The analyzer produces this diagnostic when the `Preview` constructor is
+ invoked with arguments that contain references to private symbols.
+
+ #### Example
+
+ The following code produces this diagnostic because the constant variable
+ `_name` is private to the current library:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ const String _name = 'My Foo Preview';
+
+ @Preview([!name: _name!])
+ Widget myPreview() => Text('Foo');
+ ```
+
+ #### Common fixes
+
+ If appropriate, the private symbol should be made public:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ const String name = 'My Foo Preview';
+
+ @Preview(name: name)
+ Widget myPreview() => Text('Foo');
+ ```
+
+ Otherwise, a different public constant symbol should be used:
+
+ ```dart
+ import 'package:flutter/widgets.dart';
+ import 'package:flutter/widget_previews.dart';
+
+ @Preview(name: 'My Foo Preview')
+ Widget myPreview() => Text('Foo');
+ ```
MISSING_OVERRIDE_OF_MUST_BE_OVERRIDDEN_ONE:
sharedName: MISSING_OVERRIDE_OF_MUST_BE_OVERRIDDEN
problemMessage: "Missing concrete implementation of '{0}'."
diff --git a/pkg/analyzer/test/src/diagnostics/invalid_widget_preview_application_test.dart b/pkg/analyzer/test/src/diagnostics/invalid_widget_preview_application_test.dart
new file mode 100644
index 0000000..f99da54
--- /dev/null
+++ b/pkg/analyzer/test/src/diagnostics/invalid_widget_preview_application_test.dart
@@ -0,0 +1,449 @@
+// Copyright (c) 2025, 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.g.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dart/resolution/context_collection_resolution.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(InvalidWidgetPreviewApplicationTest);
+ });
+}
+
+@reflectiveTest
+class InvalidWidgetPreviewApplicationTest extends PubPackageResolutionTest {
+ @override
+ void setUp() {
+ super.setUp();
+ writeTestPackageConfig(PackageConfigFileBuilder(), flutter: true);
+ }
+
+ // @Preview cannot be applied to constructors of abstract classes.
+ test_invalidAbstractClassConstructors() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+abstract class B extends StatelessWidget {
+ @Preview()
+ B();
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''',
+ [error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 133, 7)],
+ );
+ }
+
+ // @Preview application must invoke the `Preview` constructor.
+ test_invalidAnnotationApplication() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B extends StatelessWidget {
+ @Preview
+ B();
+}
+''',
+ [error(CompileTimeErrorCode.NO_ANNOTATION_CONSTRUCTOR_ARGUMENTS, 123, 8)],
+ );
+ }
+
+ // @Preview cannot be applied to external functions.
+ test_invalidExternalFunction() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+@Preview()
+external Widget foo();
+
+class B extends StatelessWidget {
+ @Preview()
+ external B();
+
+ @Preview()
+ external static Widget foo();
+}
+''',
+ [
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 88, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 159, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 189, 7),
+ ],
+ );
+ }
+
+ // @Preview cannot be applied to instance members of classes.
+ test_invalidInstanceMethod() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B {
+ @Preview()
+ Widget foo() {
+ return Text('Foo');
+ }
+}
+''',
+ [error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 100, 7)],
+ );
+ }
+
+ // @Preview cannot be applied to nested functions.
+ test_invalidNestedFunction() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+Widget foo() {
+ @Preview()
+ Widget nested() {
+ return Text('Foo');
+ }
+ return nested();
+}
+
+class B {
+ static Widget foo() {
+ @Preview()
+ Widget nested() {
+ return Text('Foo');
+ }
+ return nested();
+ }
+}
+''',
+ [
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 105, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 223, 7),
+ ],
+ );
+ }
+
+ // @Preview cannot be applied to:
+ //
+ // - Enums members
+ // - Extension methods
+ // - Extension type members
+ // - Mixin members
+ test_invalidParentContext() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class Foo extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) => Text('Foo');
+}
+
+enum B {
+ a,
+ b,
+ c;
+
+ @Preview()
+ const B();
+}
+
+extension on Foo {
+ @Preview()
+ Widget invalidExtensionPreview() => Text('Invalid');
+}
+
+mixin PreviewMixin {
+ @Preview()
+ Widget invalidMixinPreview() => Text('Invalid');
+}
+
+extension type FooExtensionType(Foo foo) {
+ @Preview()
+ Widget invalidExtensionTypePreview() => Text('Invalid');
+}
+''',
+ [
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 219, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 267, 7),
+ error(WarningCode.UNUSED_ELEMENT, 286, 23),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 359, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 469, 7),
+ ],
+ );
+ }
+
+ // @Preview cannot be applied to members of private classes.
+ test_invalidPrivateClass() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class _B extends StatelessWidget {
+ @Preview()
+ _B();
+
+ @Preview()
+ factory _B.foo() => _B();
+
+ @Preview()
+ static Widget bar() => Text('Bar');
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''',
+ [
+ error(WarningCode.UNUSED_ELEMENT, 93, 2),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 125, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 147, 7),
+ error(WarningCode.UNUSED_ELEMENT, 170, 3),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 189, 7),
+ error(WarningCode.UNUSED_ELEMENT, 215, 3),
+ ],
+ );
+ }
+
+ // @Preview cannot be applied to private generative or factory constructors.
+ test_invalidPrivateClassConstructors() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B extends StatelessWidget {
+ @Preview()
+ B._();
+
+ @Preview()
+ factory B._foo() => B();
+
+ B();
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''',
+ [
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 124, 7),
+ error(WarningCode.UNUSED_ELEMENT, 138, 1),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 147, 7),
+ error(WarningCode.UNUSED_ELEMENT, 169, 4),
+ ],
+ );
+ }
+
+ // @Preview cannot be applied to private static functions.
+ test_invalidPrivateClassStatic() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B extends StatelessWidget {
+ @Preview()
+ static Widget _foo() {
+ return Text('Foo');
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''',
+ [
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 124, 7),
+ error(WarningCode.UNUSED_ELEMENT, 150, 4),
+ ],
+ );
+ }
+
+ // @Preview cannot be applied to private top-level functions.
+ test_invalidPrivateTopLevel() async {
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+@Preview()
+Widget _foo() {
+ return Text('Foo');
+}
+''',
+ [
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 88, 7),
+ error(WarningCode.UNUSED_ELEMENT, 105, 4),
+ ],
+ );
+ }
+
+ // Ensure that @Preview can be applied to public factory constructors of
+ // abstract Widget subtypes.
+ test_validAbstractClassFactoryConstructor() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+abstract class B extends StatelessWidget {
+ @Preview()
+ factory B() => C();
+
+ @Preview()
+ factory B.named() => C.named();
+
+ B._();
+}
+
+class C extends B {
+ C() : super._();
+ factory C.named() => C();
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''');
+ }
+
+ // Ensure that constant instances of @Preview(...) can be applied.
+ test_validAnnotationConstant() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widget_previews.dart';
+import 'package:flutter/widgets.dart';
+
+const myPreview = Preview(name: 'Testing');
+
+@myPreview
+Widget bar() => Text('Bar');
+''');
+ }
+
+ // Ensure that @Preview can be applied to public factory constructors of
+ // Widget subtypes, including those with optional parameters.
+ test_validClassFactoryConstructor() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B extends StatelessWidget {
+ @Preview()
+ factory B.foo({Key? key}) => B._(key: key);
+
+ @Preview()
+ factory B.bar() => B._();
+
+ B._({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''');
+ }
+
+ // Ensure that @Preview can be applied to public constructors of Widget
+ // subtypes, including those with optional parameters.
+ test_validClassGenerativeConstructor() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B extends StatelessWidget {
+ @Preview()
+ const B({super.key});
+
+ @Preview()
+ B.foo([String? _]);
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''');
+ }
+
+ // Ensure that @Preview can be applied to public static functions that are
+ // defined in public classes and that return Widget or WidgetBuilder.
+ test_validClassStatic() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widgets.dart';
+import 'package:flutter/widget_previews.dart';
+
+class B extends StatelessWidget {
+ @Preview()
+ static Widget foo() {
+ return Text('Foo');
+ }
+
+ @Preview()
+ static WidgetBuilder bar() {
+ return (BuildContext context) {
+ return Text('Bar');
+ };
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container();
+ }
+}
+''');
+ }
+
+ // Ensure that @Preview can be applied to public top-level functions that
+ // return a Widget or WidgetBuilder.
+ test_validTopLevel() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widget_previews.dart';
+import 'package:flutter/widgets.dart';
+
+@Preview(name: 'Widget')
+Widget foo() => Text('Foo');
+
+@Preview(name: 'WidgetBuilder')
+WidgetBuilder bar() {
+ return (BuildContext context) {
+ return Text('Bar');
+ };
+}
+''');
+ }
+
+ // Ensure that @Preview can be applied to functions that explicitly return a
+ // subtype of Widget.
+ test_validTopLevel_widgetSubtype() async {
+ await assertNoErrorsInCode('''
+import 'package:flutter/widget_previews.dart';
+import 'package:flutter/widgets.dart';
+
+typedef MyWidget = Widget;
+
+@Preview(name: 'Testing')
+Text foo() => Text('Foo');
+
+@Preview(name: 'Testing')
+MyWidget bar() => Text('Bar');
+''');
+ }
+}
diff --git a/pkg/analyzer/test/src/diagnostics/invalid_widget_preview_private_argument_test.dart b/pkg/analyzer/test/src/diagnostics/invalid_widget_preview_private_argument_test.dart
new file mode 100644
index 0000000..1abf9bc
--- /dev/null
+++ b/pkg/analyzer/test/src/diagnostics/invalid_widget_preview_private_argument_test.dart
@@ -0,0 +1,162 @@
+// Copyright (c) 2025, 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.g.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+import '../dart/resolution/context_collection_resolution.dart';
+
+main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(InvalidWidgetPreviewPrivateArgumentTest);
+ });
+}
+
+@reflectiveTest
+class InvalidWidgetPreviewPrivateArgumentTest extends PubPackageResolutionTest {
+ @override
+ void setUp() {
+ super.setUp();
+ writeTestPackageConfig(PackageConfigFileBuilder(), flutter: true);
+ }
+
+ // @Preview cannot accept any arguments including references to private
+ // symbols.
+ test_invalidPrivatePreviewArguments() async {
+ String correctionMessageBuilder(String original, String public) {
+ return "Rename private symbol '$original' to '$public'.";
+ }
+
+ const String kPrivateName = '_privateName';
+ const String kExtraPrivateName = '__extraPrivateName';
+ const String kPrivateWidth = '_privateWidth';
+ const String kPrivateHeight = '_privateHeight';
+ const String kPrivateTextScaleFactor = '_textScaleFactor';
+ const String kPrivateWrapper = '_privateWrapper';
+ const String kPrivateTheme = '_privateTheme';
+
+ await assertErrorsInCode(
+ '''
+import 'package:flutter/widget_previews.dart';
+import 'package:flutter/widgets.dart';
+
+const String $kPrivateName = 'Name';
+const String $kExtraPrivateName = 'Extra';
+const double $kPrivateWidth = 42.0;
+const double $kPrivateHeight = 24.0;
+const double $kPrivateTextScaleFactor = 2.0;
+
+Widget $kPrivateWrapper(Widget child) => child;
+PreviewThemeData $kPrivateTheme() => PreviewThemeData();
+
+@Preview(name: $kPrivateName)
+Widget privateName() => Text('Foo');
+
+@Preview(name: '\$$kPrivateName')
+Widget privateNameStringInterp() => Text('Foo');
+
+@Preview(name: $kExtraPrivateName)
+Widget extraPrivateName() => Text('Foo');
+
+@Preview(width: $kPrivateWidth,
+ height: $kPrivateHeight,
+ textScaleFactor: $kPrivateTextScaleFactor)
+Widget privateDoubles() => Text('Foo');
+
+@Preview(width: $kPrivateWidth + 10)
+Widget numericExpressionWithPrivateDouble() => Text('Foo');
+
+@Preview(wrapper: $kPrivateWrapper)
+Widget privateWrapper() => Text('Foo');
+
+@Preview(theme: $kPrivateTheme)
+Widget privateThemeData() => Text('Foo');
+
+''',
+ [
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 388,
+ 18,
+ correctionContains: correctionMessageBuilder(
+ kPrivateName,
+ kPrivateName.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 455,
+ 21,
+ correctionContains: correctionMessageBuilder(
+ kPrivateName,
+ kPrivateName.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 537,
+ 24,
+ correctionContains: correctionMessageBuilder(
+ kExtraPrivateName,
+ kExtraPrivateName.substring(2),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 615,
+ 20,
+ correctionContains: correctionMessageBuilder(
+ kPrivateWidth,
+ kPrivateWidth.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 645,
+ 22,
+ correctionContains: correctionMessageBuilder(
+ kPrivateHeight,
+ kPrivateHeight.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 677,
+ 33,
+ correctionContains: correctionMessageBuilder(
+ kPrivateTextScaleFactor,
+ kPrivateTextScaleFactor.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 762,
+ 25,
+ correctionContains: correctionMessageBuilder(
+ kPrivateWidth,
+ kPrivateWidth.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 859,
+ 24,
+ correctionContains: correctionMessageBuilder(
+ kPrivateWrapper,
+ kPrivateWrapper.substring(1),
+ ),
+ ),
+ error(
+ WarningCode.INVALID_WIDGET_PREVIEW_PRIVATE_ARGUMENT,
+ 935,
+ 20,
+ correctionContains: correctionMessageBuilder(
+ kPrivateTheme,
+ kPrivateTheme.substring(1),
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/pkg/analyzer/test/src/diagnostics/test_all.dart b/pkg/analyzer/test/src/diagnostics/test_all.dart
index 18356d1..fc19b6d 100644
--- a/pkg/analyzer/test/src/diagnostics/test_all.dart
+++ b/pkg/analyzer/test/src/diagnostics/test_all.dart
@@ -502,6 +502,10 @@
as invalid_visible_for_overriding_annotation;
import 'invalid_visible_outside_template_annotation_test.dart'
as invalid_visible_outside_template_annotation;
+import 'invalid_widget_preview_application_test.dart'
+ as invalid_widget_preview_application;
+import 'invalid_widget_preview_private_argument_test.dart'
+ as invalid_widget_preview_private_argument;
import 'invocation_of_extension_without_call_test.dart'
as invocation_of_extension_without_call;
import 'invocation_of_non_function_expression_test.dart'
@@ -1254,6 +1258,8 @@
invalid_visibility_annotation.main();
invalid_visible_for_overriding_annotation.main();
invalid_visible_outside_template_annotation.main();
+ invalid_widget_preview_application.main();
+ invalid_widget_preview_private_argument.main();
invocation_of_extension_without_call.main();
invocation_of_non_function_expression.main();
label_in_outer_scope.main();
diff --git a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/basic.dart b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/basic.dart
index 1160b29..2c93e47 100644
--- a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/basic.dart
+++ b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/basic.dart
@@ -175,8 +175,6 @@
});
}
-typedef WidgetBuilder = Widget Function(BuildContext context);
-
class Builder extends StatelessWidget {
final WidgetBuilder builder;
diff --git a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/framework.dart b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/framework.dart
index bd5fd13..2ccf602 100644
--- a/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/framework.dart
+++ b/pkg/analyzer_utilities/lib/test/mock_packages/package_content/flutter/lib/src/widgets/framework.dart
@@ -9,6 +9,8 @@
typedef void VoidCallback();
+typedef WidgetBuilder = Widget Function(BuildContext context);
+
abstract class BuildContext {
bool get mounted;
Widget get widget;
diff --git a/pkg/linter/test/rules/unreachable_from_main_test.dart b/pkg/linter/test/rules/unreachable_from_main_test.dart
index 695326d..d6c1c40 100644
--- a/pkg/linter/test/rules/unreachable_from_main_test.dart
+++ b/pkg/linter/test/rules/unreachable_from_main_test.dart
@@ -1652,7 +1652,11 @@
Widget foo() => Text('');
}
''',
- [lint(109, 1), lint(196, 3)],
+ [
+ lint(109, 1),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 177, 7),
+ lint(196, 3),
+ ],
);
}
@@ -1685,7 +1689,10 @@
const B();
}
''',
- [lint(70, 1)],
+ [
+ lint(70, 1),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 77, 7),
+ ],
);
}
@@ -1703,7 +1710,11 @@
const B();
}
''',
- [lint(70, 1), lint(122, 1)],
+ [
+ lint(70, 1),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 77, 7),
+ lint(122, 1),
+ ],
);
}
@@ -1731,22 +1742,29 @@
''',
[
lint(168, 1),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 175, 7),
error(WarningCode.UNUSED_ELEMENT, 197, 4),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 218, 7),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 241, 7),
error(WarningCode.UNUSED_ELEMENT, 267, 4),
+ error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 291, 7),
error(WarningCode.UNUSED_ELEMENT, 306, 3),
],
);
}
test_widgetPreview_topLevelFunction() async {
- await assertNoDiagnostics(r'''
+ await assertDiagnostics(
+ r'''
import 'package:flutter/widget_previews.dart';
void main() {}
@Preview()
void f6() {}
-''');
+''',
+ [error(WarningCode.INVALID_WIDGET_PREVIEW_APPLICATION, 65, 7)],
+ );
}
test_widgetPreview_topLevelFunction_const() async {