blob: bdc6269ddb9f890a4f64d846f9d38a3dcda5bd13 [file] [log] [blame]
// 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/element.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);
}
}