blob: b6a37b6d91bf813f7186dd7e018970cff5ca332a [file]
// 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/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/error/listener.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 DiagnosticReporter _diagnosticReporter;
WidgetPreviewVerifier(this._diagnosticReporter);
/// 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,
name: parent.name?.lexeme,
parentDeclaration: parent.parent?.parent,
isExternal: parent.externalKeyword != null,
isFactory: parent.factoryKeyword != null,
parameters: parent.parameters,
),
FunctionDeclaration() => _isValidFunctionPreviewApplication(
declaration: parent,
),
MethodDeclaration() => _isValidMethodPreviewApplication(
declaration: parent,
),
PrimaryConstructorBody(:var declaration?) =>
_isValidConstructorPreviewApplication(
declaration: parent,
name: parent.declaration?.constructorName?.name.lexeme,
parentDeclaration: parent.parent?.parent,
isExternal: false,
isFactory: false,
parameters: declaration.formalParameters,
),
_ => false,
};
if (!isValidApplication) {
_diagnosticReporter.report(
diag.invalidWidgetPreviewApplication.at(node.name),
);
}
var visitor = _InvalidWidgetPreviewArgumentDetectorVisitor(
diagnosticReporter: _diagnosticReporter,
);
node.arguments!.accept(visitor);
}
bool _hasRequiredParameters(NodeList<FormalParameter> parameters) {
return parameters.any((e) => e.isRequired);
}
/// Returns whether [name] is private or `node.parent` is a [ClassDeclaration]
/// that has a private name.
bool _isPrivateContext({required String? name, required AstNode node}) {
if (name != null && Identifier.isPrivateName(name)) return true;
var parent = node.parent?.parent;
if (parent == null) return false;
var nameToken = switch (parent) {
ClassDeclaration declaration => declaration.namePart.typeName,
EnumDeclaration declaration => declaration.namePart.typeName,
ExtensionDeclaration declaration => declaration.name,
ExtensionTypeDeclaration declaration =>
declaration.primaryConstructor.typeName,
MixinDeclaration declaration => declaration.name,
_ => null,
};
if (nameToken == null) return false;
return Identifier.isPrivateName(nameToken.lexeme);
}
/// 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}) {
if (node.parent is CompilationUnit) {
return true;
}
if (node.parent case BlockClassBody body) {
if (body.parent is ClassDeclaration) {
return true;
}
}
return 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 AstNode declaration,
required String? name,
required AstNode? parentDeclaration,
required bool isExternal,
required bool isFactory,
required FormalParameterList parameters,
}) {
if (isExternal) return false;
if (parentDeclaration is! ClassDeclaration) return false;
var element = parentDeclaration.declaredFragment?.element;
if (element is! ClassElement) return false;
return !_isPrivateContext(name: name, node: declaration) &&
element.isWidget &&
!(element.isAbstract && !isFactory) &&
!_hasRequiredParameters(parameters.parameters);
}
/// 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.lexeme, 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.lexeme, node: declaration) &&
isStatic &&
// Check for nested function.
declaration.parent is! FunctionDeclarationStatement &&
externalKeyword == null &&
returnType.isValidWidgetPreviewReturnType &&
!_hasRequiredParameters(parameters);
}
return false;
}
}
class _InvalidWidgetPreviewArgumentDetectorVisitor extends RecursiveAstVisitor {
final DiagnosticReporter diagnosticReporter;
NamedArgument? rootArgument;
_InvalidWidgetPreviewArgumentDetectorVisitor({
required this.diagnosticReporter,
});
@override
void visitArgumentList(ArgumentList node) {
for (var argument in node.arguments) {
if (argument is NamedArgument) {
rootArgument = argument;
argument.argumentExpression.accept(this);
rootArgument = null;
}
}
}
@override
void visitSimpleIdentifier(SimpleIdentifier node) {
if (Identifier.isPrivateName(node.name)) {
diagnosticReporter.report(
diag.invalidWidgetPreviewPrivateArgument
.withArguments(
privateSymbolName: node.name,
suggestedName: node.name.replaceFirst(RegExp('_*'), ''),
)
.at(rootArgument!),
);
}
super.visitSimpleIdentifier(node);
}
}