linter: Introduce no_dynamic_casts replacing strict-casts

Work towards https://github.com/dart-lang/sdk/issues/63527

We will want to deprecate the `analyzer/language/strict-casts` setting,
but we first need to ship an SDK to Flutter that offers the lint rule,
before we deprecate the setting, which will cause CI to fail (like a
Dart->Flutter roll).

When the deprecation is enabled, we can also ship the automated fix.

Change-Id: I0e9651171b721577acbd416d254bca3d0324f3f9
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/509521
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Sam Rawlins
2026-06-05 07:02:01 -07:00
committed by Samuel Rawlins
parent 56e0c9fca3
commit d33ce2f88d
9 changed files with 632 additions and 0 deletions
+2
View File
@@ -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
@@ -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:
+9
View File
@@ -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<
+2
View File
@@ -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';
+2
View File
@@ -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())
@@ -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<void> {
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;
}
}
+29
View File
@@ -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:
+2
View File
@@ -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();
@@ -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<dynamic> list) {
for (int x in [!list!]) {}
}
''');
}
test_listLiteral() async {
await assertDiagnosticsFromMarkdown(r'''
void f(dynamic a) {
var list = <int>[[!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 = <int, String>{[!a!]: 'x'};
}
''');
}
test_mapLiteral_spread() async {
await assertDiagnosticsFromMarkdown(r'''
void f(dynamic a) {
var map = <String, int>{...[!a!]};
}
''');
}
test_mapLiteral_value() async {
await assertDiagnosticsFromMarkdown(r'''
void f(dynamic a) {
var map = <String, int>{'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<int> f(dynamic a) async {
return [!a!];
}
''');
}
test_setLiteral() async {
await assertDiagnosticsFromMarkdown(r'''
void f(dynamic a) {
var set = <int>{[!a!]};
}
''');
}
test_spreadList() async {
await assertDiagnosticsFromMarkdown(r'''
void f(dynamic a) {
var list = <int>[...[!a!]];
}
''');
}
test_yield() async {
await assertDiagnosticsFromMarkdown(r'''
Iterable<int> f(dynamic a) sync* {
yield [!a!];
}
''');
}
test_yieldStar() async {
await assertDiagnosticsFromMarkdown(r'''
Iterable<int> f(dynamic a) sync* {
yield* [!a!];
}
''');
}
}