diff --git a/CHANGELOG.md b/CHANGELOG.md index a3c79c44830..d0a1716be29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,8 @@ To learn more about the feature, check out the #### Analyzer +- A `no_raw_types` lint rule is introduced, which replaces the + `strict-raw-types` 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 5944795a015..ab148ce19f6 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 @@ -2290,6 +2290,11 @@ no_literal_bool_comparisons: status: hasFix no_logic_in_create_state: status: noFix +no_raw_types: + status: needsFix + notes: |- + There are a few basic cases where we can look at another expression's type + in order to find a replacement, like `C c = C()`. no_runtimetype_tostring: status: noFix no_self_assignments: diff --git a/pkg/analyzer/lib/src/analysis_options/options_file_validator.dart b/pkg/analyzer/lib/src/analysis_options/options_file_validator.dart index 68554affd22..6da38856997 100644 --- a/pkg/analyzer/lib/src/analysis_options/options_file_validator.dart +++ b/pkg/analyzer/lib/src/analysis_options/options_file_validator.dart @@ -812,23 +812,16 @@ class _LanguageOptionValidator extends OptionsValidator { var language = analyzer.valueAt(AnalysisOptionsFileKeys.language); if (language is YamlMap) { language.nodes.forEach((k, v) { - String? key; - bool validKey = false; - if (k is YamlScalar) { - key = k.value?.toString(); - if (!AnalysisOptionsFileKeys.languageOptions.contains(key)) { - _builder.reportError( - reporter, - AnalysisOptionsFileKeys.language, - k, - ); - } else { - // If we have a valid key, go on and check the value. - validKey = true; - } + if (k is! YamlScalar) return; + var key = k.value?.toString(); + if (!AnalysisOptionsFileKeys.languageOptions.contains(key)) { + _builder.reportError(reporter, AnalysisOptionsFileKeys.language, k); + return; } - if (validKey && v is YamlScalar) { - if (v.toBool() == null) { + + if (v is YamlScalar) { + var value = v.toBool(); + if (value == null) { // `null` is not a valid key, so we can safely assume `key` is // non-`null`. reporter.report( @@ -840,6 +833,17 @@ class _LanguageOptionValidator extends OptionsValidator { ) .atSourceSpan(v.span), ); + } else if (key == AnalysisOptionsFileKeys.strictRawTypes && value) { + // TODO(srawlins): Enable once the `no_raw_types` lint rule is + // available in Flutter main, so that developers have something to + // migrate to. + if (1 == 2) { + reporter.report( + diag.analysisOptionDeprecated + .withArguments(optionName: key!) + .atSourceSpan(k.span), + ); + } } } }); diff --git a/pkg/analyzer/test/src/options/options_file_validator_test.dart b/pkg/analyzer/test/src/options/options_file_validator_test.dart index f35c352d682..2843e8c67d0 100644 --- a/pkg/analyzer/test/src/options/options_file_validator_test.dart +++ b/pkg/analyzer/test/src/options/options_file_validator_test.dart @@ -308,6 +308,26 @@ analyzer: ); } + @FailingTest(reason: 'Enable when we deprecate strict-raw-types') + test_analyzer_language_strictRawTypes_deprecated() { + validate( + ''' +analyzer: + language: + strict-raw-types: true +''', + [diag.analysisOptionDeprecated], + ); + } + + test_analyzer_language_strictRawTypes_notDeprecatedIfFalse() { + validate(''' +analyzer: + language: + strict-raw-types: false +''', []); + } + test_analyzer_language_supports_empty() { validate(''' analyzer: diff --git a/pkg/linter/lib/src/diagnostic.dart b/pkg/linter/lib/src/diagnostic.dart index 97644f31214..d42d0b04df3 100644 --- a/pkg/linter/lib/src/diagnostic.dart +++ b/pkg/linter/lib/src/diagnostic.dart @@ -2,6 +2,8 @@ // 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/element/type.dart'; + import 'analyzer.dart'; import 'diagnostic.dart' as diag; diff --git a/pkg/linter/lib/src/diagnostic.g.dart b/pkg/linter/lib/src/diagnostic.g.dart index a3e4878cf35..6006c55bf21 100644 --- a/pkg/linter/lib/src/diagnostic.g.dart +++ b/pkg/linter/lib/src/diagnostic.g.dart @@ -1930,6 +1930,21 @@ const LinterLintWithoutArguments noopPrimitiveOperations = expectedTypes: [], ); +/// Parameters: +/// Type type: the name of the generic type +const DiagnosticWithArguments< + LocatableDiagnostic Function({required DartType type}) +> +noRawTypes = LinterLintTemplate( + name: 'no_raw_types', + problemMessage: + "The generic type '{0}' should have explicit type arguments but doesn't.", + correctionMessage: "Use explicit type arguments for '{0}'.", + uniqueName: 'no_raw_types', + withArguments: _withArgumentsNoRawTypes, + expectedTypes: [ExpectedType.type], +); + /// No parameters. const LinterLintWithoutArguments noRuntimetypeTostring = LinterLintWithoutArguments( @@ -4326,6 +4341,10 @@ LocatableDiagnostic _withArgumentsNonConstantIdentifierNames({ return LocatableDiagnosticImpl(diag.nonConstantIdentifierNames, [p0]); } +LocatableDiagnostic _withArgumentsNoRawTypes({required DartType type}) { + return LocatableDiagnosticImpl(diag.noRawTypes, [type]); +} + LocatableDiagnostic _withArgumentsOneMemberAbstracts({required Object p0}) { return LocatableDiagnosticImpl(diag.oneMemberAbstracts, [p0]); } diff --git a/pkg/linter/lib/src/lint_names.g.dart b/pkg/linter/lib/src/lint_names.g.dart index dc0f52bce7b..73e0f1cb6b5 100644 --- a/pkg/linter/lib/src/lint_names.g.dart +++ b/pkg/linter/lib/src/lint_names.g.dart @@ -317,6 +317,8 @@ abstract final class LintNames { static const String no_logic_in_create_state = 'no_logic_in_create_state'; + static const String no_raw_types = 'no_raw_types'; + static const String no_runtimetype_tostring = 'no_runtimetype_tostring'; static const String no_self_assignments = 'no_self_assignments'; diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart index 4a3b93b75a9..43071f7aeac 100644 --- a/pkg/linter/lib/src/rules.dart +++ b/pkg/linter/lib/src/rules.dart @@ -119,6 +119,7 @@ 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'; import 'rules/no_logic_in_create_state.dart'; +import 'rules/no_raw_types.dart'; import 'rules/no_runtimeType_toString.dart'; import 'rules/no_self_assignments.dart'; import 'rules/no_wildcard_variable_uses.dart'; @@ -385,6 +386,7 @@ void registerLintRules() { ..registerLintRule(NoLeadingUnderscoresForLocalIdentifiers()) ..registerLintRule(NoLiteralBoolComparisons()) ..registerLintRule(NoLogicInCreateState()) + ..registerLintRule(NoRawTypes()) ..registerLintRule(NoRuntimeTypeToString()) ..registerLintRule(NoSelfAssignments()) ..registerLintRule(NoWildcardVariableUses()) diff --git a/pkg/linter/lib/src/rules/no_raw_types.dart b/pkg/linter/lib/src/rules/no_raw_types.dart new file mode 100644 index 00000000000..88710dddaea --- /dev/null +++ b/pkg/linter/lib/src/rules/no_raw_types.dart @@ -0,0 +1,96 @@ +// 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/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 raw types.'; + +class NoRawTypes extends AnalysisRule { + new() : super(name: LintNames.no_raw_types, description: _desc); + + @override + DiagnosticCode get diagnosticCode => diag.noRawTypes; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + var visitor = _Visitor(this); + registry.addNamedType(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final AnalysisRule rule; + + new(this.rule); + + @override + void visitNamedType(NamedType node) { + var parent = node.parent; + if (parent is ConstructorName && + parent.parent is InstanceCreationExpression) { + return; + } + + if (node.typeArguments != null) return; + + var type = node.type; + if (type == null) return; + + var element = node.element; + if (element == null) return; + + List typeArguments; + var alias = type.alias; + if (alias != null) { + typeArguments = alias.typeArguments; + } else if (type is InterfaceType) { + typeArguments = type.typeArguments; + } else { + return; + } + + if (!typeArguments.any((t) => t is DynamicType)) return; + if (element.metadata.hasOptionalTypeArgs) return; + + if (node.parentEscapingTypeArguments + case AsExpression() || + CastPattern() || + IsExpression() || + ObjectPattern() || + TypeLiteral()) { + // Do not report a "strict raw type" warning in this case; too noisy, + // especially in the case of unstructured data parsing, like JSON and + // YAML. + return; + } + + rule.reportAtNode(node, arguments: [type]); + } +} + +extension on NamedType { + AstNode get parentEscapingTypeArguments { + var ancestor = parent!; + while (ancestor is TypeArgumentList || ancestor is NamedType) { + if (ancestor.parent case var grandancestor?) { + ancestor = grandancestor; + } else { + return ancestor; + } + } + return ancestor; + } +} diff --git a/pkg/linter/messages.yaml b/pkg/linter/messages.yaml index 04773cd69c6..71792e15e74 100644 --- a/pkg/linter/messages.yaml +++ b/pkg/linter/messages.yaml @@ -7878,6 +7878,57 @@ LinterLintCode: } } ``` + noRawTypes: + type: lint + parameters: + Type type: the name of the generic type + problemMessage: "The generic type '#type' should have explicit type arguments but doesn't." + correctionMessage: "Use explicit type arguments for '#type'." + state: + stable: "3.13" + categories: [errorProne] + hasPublishedDocs: false + documentation: |- + #### Description + + The analyzer produces this diagnostic when a generic type is used without + explicit type arguments, meaning it is a raw type. + + #### Example + + The following code produces this diagnostic because the generic class `C` + is used without a type argument: + + ```dart + class C {} + [!C!] c = C(); + ``` + + #### Common fixes + + Provide explicit type arguments: + + ```dart + class C {} + C c = C(); + ``` + deprecatedDetails: |- + **DON'T** use raw types. + + A raw type is a type annotation for a generic type which omits type + arguments. Developers may mistakenly believe that the type arguments are + inferred, but in fact each type parameter's bound is used, which can lead + to lost type information. + + **BAD:** + ```dart + List list = [1, 2, 3]; + ``` + + **GOOD:** + ```dart + List list = [1, 2, 3]; + ``` noRuntimetypeTostring: type: lint parameters: none diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart index a776ecc220e..6380767c9a5 100644 --- a/pkg/linter/test/rules/all.dart +++ b/pkg/linter/test/rules/all.dart @@ -157,6 +157,7 @@ import 'no_leading_underscores_for_local_identifiers_test.dart' as no_leading_underscores_for_local_identifiers; import 'no_literal_bool_comparisons_test.dart' as no_literal_bool_comparisons; import 'no_logic_in_create_state_test.dart' as no_logic_in_create_state; +import 'no_raw_types_test.dart' as no_raw_types; import 'no_runtimeType_toString_test.dart' as no_runtimeType_toString; import 'no_self_assignments_test.dart' as no_self_assignments; import 'no_wildcard_variable_uses_test.dart' as no_wildcard_variable_uses; @@ -454,6 +455,7 @@ void main() { no_leading_underscores_for_local_identifiers.main(); no_literal_bool_comparisons.main(); no_logic_in_create_state.main(); + no_raw_types.main(); no_runtimeType_toString.main(); no_self_assignments.main(); no_wildcard_variable_uses.main(); diff --git a/pkg/linter/test/rules/no_raw_types_test.dart b/pkg/linter/test/rules/no_raw_types_test.dart new file mode 100644 index 00000000000..cb437cfc4db --- /dev/null +++ b/pkg/linter/test/rules/no_raw_types_test.dart @@ -0,0 +1,483 @@ +// 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(NoRawTypesTest); + }); +} + +@reflectiveTest +class NoRawTypesTest extends LintRuleTest { + @override + bool get addMetaPackageDep => true; + + @override + String get lintRule => LintNames.no_raw_types; + + test_asExpression() async { + await assertNoDiagnostics(r''' +void f(dynamic x) { + print(x as List); +} +'''); + } + + test_asExpression_typeArgument() async { + await assertNoDiagnostics(r''' +void f(dynamic x) { + print(x as List); +} +'''); + } + + test_castPattern() async { + await assertNoDiagnostics(r''' +void f((Object, ) l) { + var (_ as List, ) = l; +} +'''); + } + + test_castPattern_typeArgument() async { + await assertNoDiagnostics(r''' +void f((Object, ) l) { + var (_ as List, ) = l; +} +'''); + } + + test_constantPattern() async { + await assertNoDiagnostics(r''' +void f(C c) { + switch (c) { + case const C(): + } +} + +class C { + const C(); +} +'''); + } + + test_functionParts_optionalTypeArg() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; +@optionalTypeArgs +class C {} +C f(int a) => C(); +void g(C a) {} +'''); + } + + test_genericTypeArgument_extensionType_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +extension type E(int i) {} + +void f() { + >[]; +} +'''); + } + + test_genericTypeArgument_extensionType_withTypeArg() async { + await assertNoDiagnostics(r''' +extension type E(int i) {} + +void f() { + >>[]; +} +'''); + } + + test_genericTypeArgument_extensionTypeImplements_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +extension type E(List i) implements [!Iterable!] {} +'''); + } + + test_genericTypeArgument_extensionTypeImplementsExtensionType_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +extension type E(Iterable i) {} + +extension type F(List j) implements [!E!] {} +'''); + } + + test_genericTypeArgument_extensionTypeRepresentationType_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +extension type E([!List!] i) {} +'''); + } + + test_genericTypeArgument_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void f() { + var a = <[!List!]>[]; +} +'''); + } + + test_genericTypeArgument_withTypeArg() async { + await assertNoDiagnostics(r''' +void f() { + var a = >[]; +} +'''); + } + + test_instanceCreation() async { + await assertNoDiagnostics(r''' +var c = List.empty(); +'''); + } + + test_isExpression() async { + await assertNoDiagnostics(r''' +void f(dynamic x) { + print(x is List); + print(x is List); + print(x is List); +} +'''); + } + + test_localVariable_extensionType_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +extension type E(int i) {} + +void f() { + [!E!] e = E(1); +} +'''); + } + + test_localVariable_extensionType_withTypeArg() async { + await assertNoDiagnostics(r''' +extension type E(int i) {} + +void f() { + E e = E(1); +} +'''); + } + + test_localVariable_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void f() { + [!List!] a = [1, 2, 3]; +} +'''); + } + + test_localVariable_withTypeArg() async { + await assertNoDiagnostics(r''' +void f() { + List a = [1, 2, 3]; + print(a); +} +'''); + } + + test_mixinApplication_missing() async { + await assertDiagnosticsFromMarkdown(r''' +mixin class C {} +class D = Object with [!C!]; +'''); + } + + test_mixinApplication_withTypeArg() async { + await assertNoDiagnostics(r''' +mixin class C {} +class D = Object with C; +'''); + } + + test_nonFunctionTypeAlias_explicitTypeArg() async { + await assertNoDiagnostics(''' +typedef List2 = List; +void f(List2 a) {} +'''); + } + + test_nonFunctionTypeAlias_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(''' +typedef List2 = List; +void f([!List2!] a) {} +'''); + } + + test_nonFunctionTypeAlias_optionalTypeArgs() async { + await assertNoDiagnostics(''' +import 'package:meta/meta.dart'; +@optionalTypeArgs +typedef List2 = List; +void f(List2 a) {} +'''); + } + + test_objectPattern() async { + await assertNoDiagnostics(r''' +void f(Object o) { + switch (o) { + case List(): + } +} +'''); + } + + test_parameter_default_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void f({[!List!] a = const []}) {} +'''); + } + + test_parameter_fieldFormal_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +class C { + Object a; + C([!List!] this.a); +} +'''); + } + + test_parameter_functionTyped_parameter_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void f(void a([!List!] p)) {} +'''); + } + + test_parameter_functionTyped_returnType_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void f([!List!] a()) {} +'''); + } + + test_parameter_primaryDeclaring_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +class C(final [!List!] a); +'''); + } + + test_parameter_simple_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void f([!List!] a) {} +'''); + } + + test_parameter_super_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +abstract class C { + Object a; + C(this.a); +} +class D extends C { + D([!List!] super.a); +} +'''); + } + + test_returnType_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +[!List!] f(int a) => [1, 2, 3]; +'''); + } + + test_superclassWith_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +mixin class C {} +class D extends Object with [!C!] {} +'''); + } + + test_superclassWith_withTypeArg() async { + await assertNoDiagnostics(r''' +mixin class C {} +class D extends Object with C {} +'''); + } + + test_topLevelField_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +[!List!] a = []; +'''); + } + + test_topLevelField_optionalTypeArg() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; +@optionalTypeArgs +class C {} +C a = C(); +C get g => C(); +void set s(C a) {} +'''); + } + + test_topLevelField_withTypeArg() async { + await assertNoDiagnostics(r''' +List a = []; +List get g => []; +void set s(List a) {} +'''); + } + + test_topLevelGetter_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +[!List!] get g => []; +'''); + } + + test_topLevelSetter_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +void set s([!List!] a) {} +'''); + } + + test_typeAlias_classic_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +typedef T F1(T _); +[!F1!] func = (a) => a; +'''); + } + + test_typeAlias_modern_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +typedef F1 = T Function(T); +[!F1!] func = (a) => a; +'''); + } + + test_typeAlias_modern_optionalTypeArgs() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; +@optionalTypeArgs +typedef T F1(T _); +@optionalTypeArgs +typedef F2 = T Function(T); +F1 f1 = (a) => a; +F2 f2 = (a) => a; +'''); + } + + test_typeAlias_modern_withTypeArg() async { + await assertNoDiagnostics(r''' +typedef T F1(T _); +typedef F2 = T Function(T); +typedef F3 = T Function(T); +F1 f1 = (a) => a; +F2 f2 = (a) => a; +F3 f3 = (T a) => a; +'''); + } + + test_typeInClassDeclaration_optionalTypeArgs() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; +@optionalTypeArgs +mixin class C {} +class D extends C {} +class E extends Object with C {} +class F = Object with C; +class G implements C {} +'''); + } + + test_typeInConstructorName() async { + await assertNoDiagnostics(r''' +class C { + C(); + C.named(); +} + +var c = C(); +var d = C.named(); +'''); + } + + test_typeInExtendedType_anonymous_missing() async { + await assertDiagnosticsFromMarkdown(r''' +extension on [!List!] {} +'''); + } + + test_typeInExtendedType_missing() async { + await assertDiagnosticsFromMarkdown(r''' +extension E on [!List!] {} +'''); + } + + test_typeInExtendedType_optionalTypeArgs() async { + await assertNoDiagnostics(r''' +import 'package:meta/meta.dart'; +@optionalTypeArgs +class C {} +extension E on C {} +extension on C {} +'''); + } + + test_typeInExtendedType_present() async { + await assertNoDiagnostics(r''' +extension E on List {} +extension F on List {} +'''); + } + + test_typeInInterface_missing() async { + await assertDiagnosticsFromMarkdown(r''' +class C {} +class D implements [!C!] {} +'''); + } + + test_typeInInterface_withTypeArg() async { + await assertNoDiagnostics(r''' +class C {} +class D implements C {} +'''); + } + + test_typeInSuperclass_missing() async { + await assertDiagnosticsFromMarkdown(r''' +class C {} +class D extends [!C!] {} +'''); + } + + test_typeInSuperclass_withTypeArg() async { + await assertNoDiagnostics(r''' +class C {} +class D extends C {} +'''); + } + + test_typeLiteral_raw() async { + await assertNoDiagnostics(r''' +void f() { + var t = List; + print(t); +} +'''); + } + + test_typeParameterBound_missingTypeArg() async { + await assertDiagnosticsFromMarkdown(r''' +class C {} +class D {} +'''); + } + + test_typeParameterBound_withTypeArg() async { + await assertNoDiagnostics(r''' +class C {} +class D> {} +'''); + } +}