diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a1716be29..46cf72e7a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,8 @@ To learn more about the feature, check out the - A `no_raw_types` lint rule is introduced, which replaces the `strict-raw-types` analysis option, offering a more consistent approach. +- A `no_dynamic_types` lint rule is introduced, which replaces the + `strict-casts` analysis option, offering a more consistent approach. - The following lint rules have been determined to be low value, and are deprecated: `avoid_public_typedef_functions`, and `one_member_abstracts`. If there is desire to keep using these, they can be re-implemented with 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 ab148ce19f6..181aaea2749 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 @@ -2282,6 +2282,9 @@ no_default_cases: values that the default would have matched. no_duplicate_case_values: status: hasFix +no_dynamic_casts: + status: needsFix + notes: The easy fix is to wrap the expression in a cast. no_leading_underscores_for_library_prefixes: status: hasFix no_leading_underscores_for_local_identifiers: diff --git a/pkg/linter/lib/src/diagnostic.g.dart b/pkg/linter/lib/src/diagnostic.g.dart index 6006c55bf21..0d93560744b 100644 --- a/pkg/linter/lib/src/diagnostic.g.dart +++ b/pkg/linter/lib/src/diagnostic.g.dart @@ -1851,6 +1851,15 @@ noDuplicateCaseValues = LinterLintTemplate( expectedTypes: [ExpectedType.object, ExpectedType.object], ); +/// No parameters. +const LinterLintWithoutArguments noDynamicCasts = LinterLintWithoutArguments( + name: 'no_dynamic_casts', + problemMessage: "Implicit cast from 'dynamic'.", + correctionMessage: "Try adding an explicit cast or changing the target type.", + uniqueName: 'no_dynamic_casts', + expectedTypes: [], +); + /// Parameters: /// Object p0: undocumented const DiagnosticWithArguments< diff --git a/pkg/linter/lib/src/lint_names.g.dart b/pkg/linter/lib/src/lint_names.g.dart index 73e0f1cb6b5..0020b3e276f 100644 --- a/pkg/linter/lib/src/lint_names.g.dart +++ b/pkg/linter/lib/src/lint_names.g.dart @@ -306,6 +306,8 @@ abstract final class LintNames { static const String no_duplicate_case_values = 'no_duplicate_case_values'; + static const String no_dynamic_casts = 'no_dynamic_casts'; + static const String no_leading_underscores_for_library_prefixes = 'no_leading_underscores_for_library_prefixes'; diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart index 43071f7aeac..44baad7e547 100644 --- a/pkg/linter/lib/src/rules.dart +++ b/pkg/linter/lib/src/rules.dart @@ -115,6 +115,7 @@ import 'rules/missing_whitespace_between_adjacent_strings.dart'; import 'rules/no_adjacent_strings_in_list.dart'; import 'rules/no_default_cases.dart'; import 'rules/no_duplicate_case_values.dart'; +import 'rules/no_dynamic_casts.dart'; import 'rules/no_leading_underscores_for_library_prefixes.dart'; import 'rules/no_leading_underscores_for_local_identifiers.dart'; import 'rules/no_literal_bool_comparisons.dart'; @@ -382,6 +383,7 @@ void registerLintRules() { ..registerLintRule(NoAdjacentStringsInList()) ..registerLintRule(NoDefaultCases()) ..registerLintRule(NoDuplicateCaseValues()) + ..registerLintRule(NoDynamicCasts()) ..registerLintRule(NoLeadingUnderscoresForLibraryPrefixes()) ..registerLintRule(NoLeadingUnderscoresForLocalIdentifiers()) ..registerLintRule(NoLiteralBoolComparisons()) diff --git a/pkg/linter/lib/src/rules/no_dynamic_casts.dart b/pkg/linter/lib/src/rules/no_dynamic_casts.dart new file mode 100644 index 00000000000..07c84113cfa --- /dev/null +++ b/pkg/linter/lib/src/rules/no_dynamic_casts.dart @@ -0,0 +1,347 @@ +// Copyright (c) 2026, 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/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/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart'; + +import '../analyzer.dart'; +import '../diagnostic.dart' as diag; + +const _desc = r'Avoid implicit casts from `dynamic`.'; + +class NoDynamicCasts extends AnalysisRule { + new() : super(name: LintNames.no_dynamic_casts, description: _desc); + + @override + DiagnosticCode get diagnosticCode => diag.noDynamicCasts; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + var visitor = _Visitor(this, context); + registry + ..addArgumentList(this, visitor) + ..addAssignmentExpression(this, visitor) + ..addBinaryExpression(this, visitor) + ..addConditionalExpression(this, visitor) + ..addDoStatement(this, visitor) + ..addExpressionFunctionBody(this, visitor) + ..addForEachPartsWithDeclaration(this, visitor) + ..addForEachPartsWithIdentifier(this, visitor) + ..addForEachPartsWithPattern(this, visitor) + ..addForStatement(this, visitor) + ..addIfElement(this, visitor) + ..addIfStatement(this, visitor) + ..addListLiteral(this, visitor) + ..addPrefixExpression(this, visitor) + ..addReturnStatement(this, visitor) + ..addSetOrMapLiteral(this, visitor) + ..addVariableDeclaration(this, visitor) + ..addWhenClause(this, visitor) + ..addWhileStatement(this, visitor) + ..addYieldStatement(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final AnalysisRule _rule; + final RuleContext _context; + + new(this._rule, this._context); + + @override + void visitArgumentList(ArgumentList node) { + for (var argument in node.arguments) { + _check( + argument.argumentExpression, + argument.correspondingParameter?.type, + ); + } + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _check(node.rightHandSide, node.writeType); + } + + @override + void visitBinaryExpression(BinaryExpression node) { + if (node.operator.type == TokenType.AMPERSAND_AMPERSAND || + node.operator.type == TokenType.BAR_BAR) { + _check(node.leftOperand, _context.typeProvider.boolType); + _check(node.rightOperand, _context.typeProvider.boolType); + } + } + + @override + void visitConditionalExpression(ConditionalExpression node) { + _check(node.condition, _context.typeProvider.boolType); + } + + @override + void visitDoStatement(DoStatement node) { + _check(node.condition, _context.typeProvider.boolType); + } + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) { + var returnType = _getEnclosingReturnType(node); + _check(node.expression, returnType); + } + + @override + void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) { + _checkForEachParts(node); + } + + @override + void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) { + _checkForEachParts(node); + } + + @override + void visitForEachPartsWithPattern(ForEachPartsWithPattern node) { + _checkForEachParts(node); + } + + @override + void visitForStatement(ForStatement node) { + if (node.forLoopParts case ForParts parts) { + var condition = parts.condition; + if (condition != null) { + _check(condition, _context.typeProvider.boolType); + } + } + } + + @override + void visitIfElement(IfElement node) { + _check(node.expression, _context.typeProvider.boolType); + } + + @override + void visitIfStatement(IfStatement node) { + _check(node.expression, _context.typeProvider.boolType); + } + + @override + void visitListLiteral(ListLiteral node) { + var type = node.staticType; + if (type case ParameterizedType(typeArguments: [var elementType])) { + for (var element in node.elements) { + _checkCollectionElement(element, elementType); + } + } + } + + @override + void visitPrefixExpression(PrefixExpression node) { + if (node.operator.type == TokenType.BANG) { + _check(node.operand, _context.typeProvider.boolType); + } + } + + @override + void visitReturnStatement(ReturnStatement node) { + if (node.expression case var expression?) { + var returnType = _getEnclosingReturnType(node); + _check(expression, returnType); + } + } + + @override + void visitSetOrMapLiteral(SetOrMapLiteral node) { + var type = node.staticType; + if (type is! ParameterizedType) return; + if (node.isSet && type.typeArguments.isNotEmpty) { + var elementType = type.typeArguments[0]; + for (var element in node.elements) { + _checkCollectionElement(element, elementType); + } + } else if (node.isMap && type.typeArguments.length == 2) { + var keyType = type.typeArguments[0]; + var valueType = type.typeArguments[1]; + for (var element in node.elements) { + _checkCollectionElementMap(element, keyType, valueType); + } + } + } + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (node.initializer case var initializer?) { + var parent = node.parent; + if (parent case VariableDeclarationList(:var type?)) { + _check(initializer, type.type); + } + } + } + + @override + void visitWhenClause(WhenClause node) { + _check(node.expression, _context.typeProvider.boolType); + } + + @override + void visitWhileStatement(WhileStatement node) { + _check(node.condition, _context.typeProvider.boolType); + } + + @override + void visitYieldStatement(YieldStatement node) { + var returnType = _getEnclosingReturnType(node); + if (returnType == null) return; + + if (node.star != null) { + _check(node.expression, returnType); + } else { + if (returnType case ParameterizedType(typeArguments: [var elementType])) { + _check(node.expression, elementType); + } + } + } + + /// Reports lint if [expression] is `dynamic`-typed and [targetType] is + /// neither `dynamic` nor `Object?`. + void _check(Expression expression, DartType? targetType) { + if (targetType == null) return; + + var sourceType = expression.staticType; + if (sourceType is! DynamicType) return; + + if (targetType is DynamicType) return; + if (targetType == _context.typeProvider.objectQuestionType) return; + + // Ignore if the expression is an explicit cast. + if (expression.unParenthesized is AsExpression) return; + + _rule.reportAtNode(expression); + } + + /// Checks [element], as an element in a List or Set literal, for + /// `dynamic`-typed sub-elements. + void _checkCollectionElement( + CollectionElement element, + DartType elementType, + ) { + switch (element) { + case Expression(): + _check(element, elementType); + case IfElement(): + _checkCollectionElement(element.thenElement, elementType); + if (element.elseElement case var elseElement?) { + _checkCollectionElement(elseElement, elementType); + } + case ForElement(): + _checkCollectionElement(element.body, elementType); + case SpreadElement(): + var expectedType = _context.typeProvider.iterableType(elementType); + _check(element.expression, expectedType); + default: + break; + } + } + + /// Checks [element], as an element in a Map literal, for `dynamic`-typed + /// sub-elements. + void _checkCollectionElementMap( + CollectionElement element, + DartType keyType, + DartType valueType, + ) { + switch (element) { + case MapLiteralEntry(): + _check(element.key, keyType); + _check(element.value, valueType); + case IfElement(): + _checkCollectionElementMap(element.thenElement, keyType, valueType); + if (element.elseElement case var elseElement?) { + _checkCollectionElementMap(elseElement, keyType, valueType); + } + case ForElement(): + _checkCollectionElementMap(element.body, keyType, valueType); + case SpreadElement(): + var expectedType = _context.typeProvider.mapType(keyType, valueType); + _check(element.expression, expectedType); + default: + break; + } + } + + /// Checks [node] for `dynamic`-typed sub-expressions. + void _checkForEachParts(ForEachParts node) { + var forStatement = node.parent; + if (forStatement is! ForStatement) return; + var isAsync = forStatement.awaitKeyword != null; + var targetType = isAsync + ? _context.typeProvider.streamType(_context.typeProvider.dynamicType) + : _context.typeProvider.iterableType(_context.typeProvider.dynamicType); + _check(node.iterable, targetType); + + // Also check loop variable assignment. + DartType? loopVarType; + if (node is ForEachPartsWithDeclaration) { + loopVarType = node.loopVariable.type?.type; + } else if (node is ForEachPartsWithIdentifier) { + loopVarType = node.identifier.staticType; + } + if (loopVarType == null) return; + if (loopVarType is DynamicType || loopVarType is VoidType) return; + + var iterableType = node.iterable.staticType; + DartType? elementType; + if (iterableType is ParameterizedType && + iterableType.typeArguments.isNotEmpty) { + elementType = iterableType.typeArguments[0]; + } else if (iterableType is DynamicType) { + elementType = iterableType; + } + if (elementType is! DynamicType) return; + if (targetType is DynamicType) return; + if (targetType == _context.typeProvider.objectQuestionType) return; + + _rule.reportAtNode(node.iterable); + } + + DartType? _getEnclosingReturnType(AstNode node) { + var parent = node.thisOrAncestorMatching( + (e) => + e is FunctionDeclaration || + e is MethodDeclaration || + e is ConstructorDeclaration || + e is FunctionExpression, + ); + if (parent == null) return null; + + DartType? returnType; + bool isAsync = false; + + if (parent is FunctionDeclaration) { + returnType = parent.declaredFragment?.element.returnType; + isAsync = parent.functionExpression.body.isAsynchronous; + } else if (parent is MethodDeclaration) { + returnType = parent.declaredFragment?.element.returnType; + isAsync = parent.body.isAsynchronous; + } else if (parent is ConstructorDeclaration) { + returnType = parent.declaredFragment?.element.returnType; + } else if (parent is FunctionExpression) { + if (parent.staticType case FunctionType staticType) { + returnType = staticType.returnType; + } + isAsync = parent.body.isAsynchronous; + } + + if (returnType == null) return null; + + return isAsync ? _context.typeSystem.flatten(returnType) : returnType; + } +} diff --git a/pkg/linter/messages.yaml b/pkg/linter/messages.yaml index 71792e15e74..8b915b3273d 100644 --- a/pkg/linter/messages.yaml +++ b/pkg/linter/messages.yaml @@ -7619,6 +7619,35 @@ LinterLintCode: NOTE: this lint only reports duplicate cases in libraries opted in to Dart 2.19 and below. In Dart 3.0 and after, duplicate cases are reported as dead code by the analyzer. + noDynamicCasts: + type: lint + parameters: none + problemMessage: "Implicit cast from 'dynamic'." + correctionMessage: "Try adding an explicit cast or changing the target type." + state: + stable: "3.13" + categories: [errorProne] + hasPublishedDocs: false + deprecatedDetails: |- + **DO** avoid implicit casts from `dynamic`. + + Assigning a `dynamic`-typed expression to a non-`dynamic`, non-`Object?` + target is a form of implicit casting. It is better to make such a cast + explicit, so that it is visible to developers. + + **BAD:** + ```dart + void f(dynamic x) { + int y = x; + } + ``` + + **GOOD:** + ```dart + void f(dynamic x) { + int y = x as int; + } + ``` noLeadingUnderscoresForLibraryPrefixes: type: lint parameters: diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart index 6380767c9a5..20b83214206 100644 --- a/pkg/linter/test/rules/all.dart +++ b/pkg/linter/test/rules/all.dart @@ -151,6 +151,7 @@ import 'missing_whitespace_between_adjacent_strings_test.dart' import 'no_adjacent_strings_in_list_test.dart' as no_adjacent_strings_in_list; import 'no_default_cases_test.dart' as no_default_cases; import 'no_duplicate_case_values_test.dart' as no_duplicate_case_values; +import 'no_dynamic_casts_test.dart' as no_dynamic_casts; import 'no_leading_underscores_for_library_prefixes_test.dart' as no_leading_underscores_for_library_prefixes; import 'no_leading_underscores_for_local_identifiers_test.dart' @@ -451,6 +452,7 @@ void main() { no_adjacent_strings_in_list.main(); no_default_cases.main(); no_duplicate_case_values.main(); + no_dynamic_casts.main(); no_leading_underscores_for_library_prefixes.main(); no_leading_underscores_for_local_identifiers.main(); no_literal_bool_comparisons.main(); diff --git a/pkg/linter/test/rules/no_dynamic_casts_test.dart b/pkg/linter/test/rules/no_dynamic_casts_test.dart new file mode 100644 index 00000000000..f8d82ff1248 --- /dev/null +++ b/pkg/linter/test/rules/no_dynamic_casts_test.dart @@ -0,0 +1,236 @@ +// Copyright (c) 2026, 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:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoDynamicCastsTest); + }); +} + +@reflectiveTest +class NoDynamicCastsTest extends LintRuleTest { + @override + String get lintRule => LintNames.no_dynamic_casts; + + test_argument() async { + await assertDiagnosticsFromMarkdown(r''' +void f(int x) {} +void g(dynamic a) { + f([!a!]); +} +'''); + } + + test_assignment() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + int x = [!a!]; +} +'''); + } + + test_assignment_ok() async { + await assertNoDiagnostics(r''' +void f(dynamic a) { + dynamic x = a; + Object? y = a; +} +'''); + } + + test_condition_conditionalExpression() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + [!a!] ? 1 : 2; +} +'''); + } + + test_condition_doLoop() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + do {} while ([!a!]); +} +'''); + } + + test_condition_forLoop() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + for (; [!a!];) {} +} +'''); + } + + test_condition_ifExpression() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + [if ([!a!]) 7]; +} +'''); + } + + test_condition_ifStatement() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + if ([!a!]) {} +} +'''); + } + + test_condition_whileLoop() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + while ([!a!]) {} +} +'''); + } + + test_explicitCast_ok() async { + await assertNoDiagnostics(r''' +void f(dynamic a) { + int x = a as int; +} +'''); + } + + test_expressionFunctionBody() async { + await assertDiagnosticsFromMarkdown(r''' +int f(dynamic a) => [!a!]; +'''); + } + + test_forEach_iterable() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + for (var x in [!a!]) {} +} +'''); + } + + test_forEach_variable() async { + await assertDiagnosticsFromMarkdown(r''' +void f(List list) { + for (int x in [!list!]) {} +} +'''); + } + + test_listLiteral() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + var list = [[!a!]]; +} +'''); + } + + test_logicalBinary_left() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a, bool b) { + [!a!] && b; +} +'''); + } + + test_logicalBinary_right() async { + await assertDiagnosticsFromMarkdown(r''' +void f(bool a, dynamic b) { + a && [!b!]; +} +'''); + } + + test_mapLiteral_key() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + var map = {[!a!]: 'x'}; +} +'''); + } + + test_mapLiteral_spread() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + var map = {...[!a!]}; +} +'''); + } + + test_mapLiteral_value() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + var map = {'x': [!a!]}; +} +'''); + } + + test_namedArgument() async { + await assertDiagnosticsFromMarkdown(r''' +void f({required int x}) {} +void g(dynamic a) { + f(x: [!a!]); +} +'''); + } + + test_negation() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + ![!a!]; +} +'''); + } + + test_return() async { + await assertDiagnosticsFromMarkdown(r''' +int f(dynamic a) { + return [!a!]; +} +'''); + } + + test_return_async() async { + await assertDiagnosticsFromMarkdown(r''' +Future f(dynamic a) async { + return [!a!]; +} +'''); + } + + test_setLiteral() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + var set = {[!a!]}; +} +'''); + } + + test_spreadList() async { + await assertDiagnosticsFromMarkdown(r''' +void f(dynamic a) { + var list = [...[!a!]]; +} +'''); + } + + test_yield() async { + await assertDiagnosticsFromMarkdown(r''' +Iterable f(dynamic a) sync* { + yield [!a!]; +} +'''); + } + + test_yieldStar() async { + await assertDiagnosticsFromMarkdown(r''' +Iterable f(dynamic a) sync* { + yield* [!a!]; +} +'''); + } +}