From 172e28f1d9dfaf92c63ff7de014be60f378258e1 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Wed, 29 Jan 2025 22:00:36 -0600 Subject: [PATCH] Add tooling to programmatically update diagnostic-related files (#6368) This mostly moves some tooling from the SDK and doesn't need to be too closely reviewed. It adds `./dash_site generate-diagnostics` to update the `src/_data/linter_rules.json` and `src/content/tools/diagnostic-messages.md` files from sources in the SDK. **This is temporary and much of it will be expanded or replaced** with logic in the static site generator to generate individual diagnostic message pages. I'm proposing we land this so we can incrementally get to that point and enable simplifying the SDK implementation. --- src/_data/linter_rules.json | 28 +- src/content/tools/diagnostic-messages.md | 39 +- tool/dart_site/lib/dart_site.dart | 2 + .../commands/generate_diagnostic_docs.dart | 39 ++ .../lib/src/diagnostics/diagnostics.dart | 260 +++++++++++++ .../error_code_documentation_info.dart | 296 +++++++++++++++ .../lib/src/diagnostics/error_code_info.dart | 345 ++++++++++++++++++ .../dart_site/lib/src/diagnostics/linter.dart | 57 +++ tool/dart_site/lib/src/utils.dart | 12 + tool/dart_site/pubspec.yaml | 2 + 10 files changed, 1062 insertions(+), 18 deletions(-) create mode 100644 tool/dart_site/lib/src/commands/generate_diagnostic_docs.dart create mode 100644 tool/dart_site/lib/src/diagnostics/diagnostics.dart create mode 100644 tool/dart_site/lib/src/diagnostics/error_code_documentation_info.dart create mode 100644 tool/dart_site/lib/src/diagnostics/error_code_info.dart create mode 100644 tool/dart_site/lib/src/diagnostics/linter.dart diff --git a/src/_data/linter_rules.json b/src/_data/linter_rules.json index a6fcce0b1f..9a8749d6d9 100644 --- a/src/_data/linter_rules.json +++ b/src/_data/linter_rules.json @@ -65,7 +65,7 @@ ], "sets": [], "fixStatus": "hasFix", - "details": "From the [style guide for the flutter repo](https://flutter.dev/style-guide/):\n\n**DO** specify type annotations.\n\nAvoid `var` when specifying that a type is unknown and short-hands that elide\ntype annotations. Use `dynamic` if you are being explicit that the type is\nunknown. Use `Object` if you are being explicit that you want an object that\nimplements `==` and `hashCode`.\n\n**BAD:**\n```dart\nvar foo = 10;\nfinal bar = Bar();\nconst quux = 20;\n```\n\n**GOOD:**\n```dart\nint foo = 10;\nfinal Bar bar = Bar();\nString baz = 'hello';\nconst int quux = 20;\n```\n\nNOTE: Using the the `@optionalTypeArgs` annotation in the `meta` package, API\nauthors can special-case type parameters whose type needs to be dynamic but whose\ndeclaration should be treated as optional. For example, suppose you have a\n`Key` object whose type parameter you'd like to treat as optional. Using the\n`@optionalTypeArgs` would look like this:\n\n```dart\nimport 'package:meta/meta.dart';\n\n@optionalTypeArgs\nclass Key {\n ...\n}\n\nvoid main() {\n Key s = Key(); // OK!\n}\n```", + "details": "From the [style guide for the flutter repo](https://flutter.dev/style-guide/):\n\n**DO** specify type annotations.\n\nAvoid `var` when specifying that a type is unknown and short-hands that elide\ntype annotations. Use `dynamic` if you are being explicit that the type is\nunknown. Use `Object` if you are being explicit that you want an object that\nimplements `==` and `hashCode`.\n\n**BAD:**\n```dart\nvar foo = 10;\nfinal bar = Bar();\nconst quux = 20;\n```\n\n**GOOD:**\n```dart\nint foo = 10;\nfinal Bar bar = Bar();\nString baz = 'hello';\nconst int quux = 20;\n```\n\nNOTE: Using the `@optionalTypeArgs` annotation in the `meta` package, API\nauthors can special-case type parameters whose type needs to be dynamic but whose\ndeclaration should be treated as optional. For example, suppose you have a\n`Key` object whose type parameter you'd like to treat as optional. Using the\n`@optionalTypeArgs` would look like this:\n\n```dart\nimport 'package:meta/meta.dart';\n\n@optionalTypeArgs\nclass Key {\n ...\n}\n\nvoid main() {\n Key s = Key(); // OK!\n}\n```", "sinceDartSdk": "2.0" }, { @@ -1218,7 +1218,7 @@ "flutter" ], "fixStatus": "noFix", - "details": "From the the [pub package layout doc](https://dart.dev/tools/pub/package-layout#implementation-files):\n\n**DON'T** import implementation files from another package.\n\nThe libraries inside `lib` are publicly visible: other packages are free to\nimport them. But much of a package's code is internal implementation libraries\nthat should only be imported and used by the package itself. Those go inside a\nsubdirectory of `lib` called `src`. You can create subdirectories in there if\nit helps you organize things.\n\nYou are free to import libraries that live in `lib/src` from within other Dart\ncode in the same package (like other libraries in `lib`, scripts in `bin`,\nand tests) but you should never import from another package's `lib/src`\ndirectory. Those files are not part of the package's public API, and they\nmight change in ways that could break your code.\n\n**BAD:**\n```dart\n// In 'road_runner'\nimport 'package:acme/src/internals.dart';\n```", + "details": "From the [pub package layout doc](https://dart.dev/tools/pub/package-layout#implementation-files):\n\n**DON'T** import implementation files from another package.\n\nThe libraries inside `lib` are publicly visible: other packages are free to\nimport them. But much of a package's code is internal implementation libraries\nthat should only be imported and used by the package itself. Those go inside a\nsubdirectory of `lib` called `src`. You can create subdirectories in there if\nit helps you organize things.\n\nYou are free to import libraries that live in `lib/src` from within other Dart\ncode in the same package (like other libraries in `lib`, scripts in `bin`,\nand tests) but you should never import from another package's `lib/src`\ndirectory. Those files are not part of the package's public API, and they\nmight change in ways that could break your code.\n\n**BAD:**\n```dart\n// In 'road_runner'\nimport 'package:acme/src/internals.dart';\n```", "sinceDartSdk": "2.0" }, { @@ -1724,7 +1724,7 @@ "sets": [], "fixStatus": "hasFix", "details": "Don't type annotate initialized top-level or static variables when the type is\nobvious.\n\n**BAD:**\n```dart\nfinal int myTopLevelVariable = 7;\n\nclass A {\n static String myStaticVariable = 'Hello';\n}\n```\n\n**GOOD:**\n```dart\nfinal myTopLevelVariable = 7;\n\nclass A {\n static myStaticVariable = 'Hello';\n}\n```\n\nSometimes the inferred type is not the type you want the variable to have. For\nexample, you may intend to assign values of other types later. You may also\nwish to write a type annotation explicitly because the type of the initializing\nexpression is non-obvious and it will be helpful for future readers of the\ncode to document this type. Or you may wish to commit to a specific type such\nthat future updates of dependencies (in nearby code, in imports, anywhere)\nwill not silently change the type of that variable, thus introducing\ncompile-time errors or run-time bugs in locations where this variable is used.\nIn those cases, go ahead and annotate the variable with the type you want.\n\n**GOOD:**\n```dart\nfinal num myTopLevelVariable = 7;\n\nclass A {\n static String? myStaticVariable = 'Hello';\n}\n```\n\n**This rule is experimental.** It is being evaluated, and it may be changed\nor removed. Feedback on its behavior is welcome! The main issue is here:\nhttps://github.com/dart-lang/linter/issues/5101.", - "sinceDartSdk": "3.7-wip" + "sinceDartSdk": "3.7" }, { "name": "one_member_abstracts", @@ -1777,11 +1777,11 @@ "effectiveDart", "publicInterface" ], - "state": "deprecated", + "state": "removed", "incompatible": [], "sets": [], "fixStatus": "noFix", - "details": "**NOTE:** This lint is deprecated because it is has not\nbeen fully functional since at least Dart 2.0.\nRemove all inclusions of this lint from your analysis options.\n\n**DO** provide doc comments for all public APIs.\n\nAs described in the [pub package layout doc](https://dart.dev/tools/pub/package-layout#implementation-files),\npublic APIs consist in everything in your package's `lib` folder, minus\nimplementation files in `lib/src`, adding elements explicitly exported with an\n`export` directive.\n\nFor example, given `lib/foo.dart`:\n```dart\nexport 'src/bar.dart' show Bar;\nexport 'src/baz.dart';\n\nclass Foo { }\n\nclass _Foo { }\n```\nits API includes:\n\n* `Foo` (but not `_Foo`)\n* `Bar` (exported) and\n* all *public* elements in `src/baz.dart`\n\nAll public API members should be documented with `///` doc-style comments.\n\n**BAD:**\n```dart\nclass Bar {\n void bar();\n}\n```\n\n**GOOD:**\n```dart\n/// A Foo.\nabstract class Foo {\n /// Start foo-ing.\n void start() => _start();\n\n _start();\n}\n```\n\nAdvice for writing good doc comments can be found in the\n[Doc Writing Guidelines](https://dart.dev/effective-dart/documentation).", + "details": "**NOTE:** This lint has been removed because it is has not\nbeen fully functional since at least Dart 2.0.\nRemove all inclusions of this lint from your analysis options.\n\n**DO** provide doc comments for all public APIs.\n\nAs described in the [pub package layout doc](https://dart.dev/tools/pub/package-layout#implementation-files),\npublic APIs consist in everything in your package's `lib` folder, minus\nimplementation files in `lib/src`, adding elements explicitly exported with an\n`export` directive.\n\nFor example, given `lib/foo.dart`:\n```dart\nexport 'src/bar.dart' show Bar;\nexport 'src/baz.dart';\n\nclass Foo { }\n\nclass _Foo { }\n```\nits API includes:\n\n* `Foo` (but not `_Foo`)\n* `Bar` (exported) and\n* all *public* elements in `src/baz.dart`\n\nAll public API members should be documented with `///` doc-style comments.\n\n**BAD:**\n```dart\nclass Bar {\n void bar();\n}\n```\n\n**GOOD:**\n```dart\n/// A Foo.\nabstract class Foo {\n /// Start foo-ing.\n void start() => _start();\n\n _start();\n}\n```\n\nAdvice for writing good doc comments can be found in the\n[Doc Writing Guidelines](https://dart.dev/effective-dart/documentation).", "sinceDartSdk": "2.0" }, { @@ -2653,8 +2653,8 @@ "incompatible": [], "sets": [], "fixStatus": "hasFix", - "details": "Do type annotate initialized top-level or static variables when the type is\nnon-obvious.\n\nType annotations on top-level or static variables can serve as a request for\ntype inference, documenting the expected outcome of the type inference step,\nand declaratively allowing the compiler and analyzer to solve the possibly\ncomplex task of finding type arguments and annotations in the initializing\nexpression that yield the desired result.\n\nType annotations on top-level or static variables can also inform readers about\nthe type of the initializing expression, which will allow them to proceed\nreading the locations in code where this variable is used with known good\ninformation about the type of the given variable (which may not be immediately\nevident by looking at the initializing expression).\n\nAn expression is considered to have a non-obvious type when it does not\nhave an obvious type.\n\nAn expression e has an obvious type in the following cases:\n\n- e is a non-collection literal. For instance, 1, true, 'Hello, $name!'.\n- e is a collection literal with actual type arguments. For instance,\n {}.\n- e is a list literal or a set literal where at least one element has an\n obvious type, and all elements have the same type. For instance, [1, 2] and\n { [true, false], [] }, but not [1, 1.5].\n- e is a map literal where all key-value pair have a key with an obvious type\n and a value with an obvious type, and all keys have the same type, and all\n values have the same type. For instance, { #a: [] }, but not\n {1: 1, 2: true}.\n- e is an instance creation expression whose class part is not raw. For\n instance C(14) if C is a non-generic class, or C(14) if C accepts one\n type argument, but not C(14) if C accepts one or more type arguments.\n- e is a cascade whose target has an obvious type. For instance,\n 1..isEven..isEven has an obvious type because 1 has an obvious type.\n- e is a type cast. For instance, myComplexpression as int.\n\n**BAD:**\n```dart\nfinal myTopLevelVariable =\n genericFunctionWrittenByOtherFolks(with, args);\n\nclass A {\n static var myStaticVariable =\n myTopLevelVariable.update('foo', null);\n}\n```\n\n**GOOD:**\n```dart\nfinal Map myTopLevelVariable =\n genericFunctionWrittenByOtherFolks(with, args);\n\nclass A {\n static Map myStaticVariable =\n myTopLevelVariable.update('foo', null);\n}\n```\n\n**This rule is experimental.** It is being evaluated, and it may be changed\nor removed. Feedback on its behavior is welcome! The main issue is here:\nhttps://github.com/dart-lang/linter/issues/5101.", - "sinceDartSdk": "3.7-wip" + "details": "Do type annotate initialized top-level or static variables when the type is\nnon-obvious.\n\nType annotations on top-level or static variables can serve as a request for\ntype inference, documenting the expected outcome of the type inference step,\nand declaratively allowing the compiler and analyzer to solve the possibly\ncomplex task of finding type arguments and annotations in the initializing\nexpression that yield the desired result.\n\nType annotations on top-level or static variables can also inform readers about\nthe type of the initializing expression, which will allow them to proceed\nreading the locations in code where this variable is used with known good\ninformation about the type of the given variable (which may not be immediately\nevident by looking at the initializing expression).\n\nAn expression is considered to have a non-obvious type when it does not\nhave an obvious type.\n\nAn expression e has an obvious type in the following cases:\n\n- e is a non-collection literal. For instance, 1, true, 'Hello, $name!'.\n- e is a collection literal with actual type arguments. For instance,\n {}.\n- e is a list literal or a set literal where at least one element has an\n obvious type, and all elements have the same type. For instance, [1, 2] and\n { [true, false], [] }, but not [1, 1.5].\n- e is a map literal where all key-value pair have a key with an obvious type\n and a value with an obvious type, and all keys have the same type, and all\n values have the same type. For instance, { #a: [] }, but not\n {1: 1, 2: true}.\n- e is an instance creation expression whose class part is not raw. For\n instance C(14) if C is a non-generic class, or C(14) if C accepts one\n type argument, but not C(14) if C accepts one or more type arguments.\n- e is a cascade whose target has an obvious type. For instance,\n 1..isEven..isEven has an obvious type because 1 has an obvious type.\n- e is a type cast. For instance, `myComplexExpression as int`.\n\n**BAD:**\n```dart\nfinal myTopLevelVariable =\n genericFunctionWrittenByOtherFolks(with, args);\n\nclass A {\n static var myStaticVariable =\n myTopLevelVariable.update('foo', null);\n}\n```\n\n**GOOD:**\n```dart\nfinal Map myTopLevelVariable =\n genericFunctionWrittenByOtherFolks(with, args);\n\nclass A {\n static Map myStaticVariable =\n myTopLevelVariable.update('foo', null);\n}\n```\n\n**This rule is experimental.** It is being evaluated, and it may be changed\nor removed. Feedback on its behavior is welcome! The main issue is here:\nhttps://github.com/dart-lang/linter/issues/5101.", + "sinceDartSdk": "3.7" }, { "name": "strict_top_level_inference", @@ -2667,7 +2667,7 @@ "sets": [], "fixStatus": "hasFix", "details": "Do type annotate top-level and class-like member declarations, where types\nare not inferred from super-interfaces or initializers.\n\nThe lint warns about every omitted return type, parameter type, and\nvariable type of a top-level declaration or class-like-namespace-level\ndeclaration (static or instance member or constructor declaration), which\nis not given a type by inference, and which therefore defaults to dynamic.\n\nThe only omitted types that can be given a type by top-level inference,\nare those of variable declarations with initializer expressions, and\nreturn and parameter types of instance members that override a consistent\ncombined super-interface signature.\n\nSetters do not need a return type, as it is always assumed to be `void`.\n\n**BAD:**\n```dart\nvar _zeroPointCache;\nclass Point {\n get zero => ...;\n final x, y;\n Point(x, y) {}\n closest(b, c) => distance(b) <= distance(c) ? b : c;\n distance(other) => ...;\n}\n_sq(v) => v * v;\n```\n\n**GOOD:**\n```dart\nPoint? _zeroPointCache;\nclass Point {\n Point get zero => ...;\n final int x, y;\n Point(int x, int y) {}\n closest(Point b, Point c) =>\n distance(b) <= distance(c) ? b : c;\n distance(Point other) => ...;\n}\nint _sq(int v) => v * v;\n```", - "sinceDartSdk": "3.7-wip" + "sinceDartSdk": "3.7" }, { "name": "super_goes_last", @@ -2838,7 +2838,7 @@ "incompatible": [], "sets": [], "fixStatus": "hasFix", - "details": "Only use a `break` in a non-empty switch case statement if you need to break\nbefore the end of the case body. Dart does not support fallthrough execution\nfor non-empty cases, so `break`s at the end of non-empty switch case statements\nare unnecessary.\n\n**BAD:**\n```dart\nswitch (1) {\n case 1:\n print(\"one\");\n break;\n case 2:\n print(\"two\");\n break;\n}\n```\n\n**GOOD:**\n```dart\nswitch (1) {\n case 1:\n print(\"one\");\n case 2:\n print(\"two\");\n}\n```\n\n```dart\nswitch (1) {\n case 1:\n case 2:\n print(\"one or two\");\n}\n```\n\n```dart\nswitch (1) {\n case 1:\n break;\n case 2:\n print(\"just two\");\n}\n```\n\nNOTE: This lint only reports unnecessary breaks in libraries with a\n[language version](https://dart.dev/resources/language/evolution#language-versioning)\nof 3.0 or greater. Explicit breaks are still required in Dart 2.19 and below.", + "details": "Only use a `break` in a non-empty switch case statement if you need to break\nbefore the end of the case body. Dart does not support fallthrough execution\nfor non-empty cases, so `break`s at the end of non-empty switch case statements\nare unnecessary.\n\n**BAD:**\n```dart\nswitch (1) {\n case 1:\n print(\"one\");\n break;\n case 2:\n print(\"two\");\n break;\n}\n```\n\n**GOOD:**\n```dart\nswitch (1) {\n case 1:\n print(\"one\");\n case 2:\n print(\"two\");\n}\n```\n\n```dart\nswitch (1) {\n case 1:\n case 2:\n print(\"one or two\");\n}\n```\n\n```dart\nswitch (1) {\n case 1:\n break;\n case 2:\n print(\"just two\");\n}\n```\n\nNOTE: This lint only reports unnecessary breaks in libraries with a\n[language version](https://dart.dev/guides/language/evolution#language-versioning)\nof 3.0 or greater. Explicit breaks are still required in Dart 2.19 and below.", "sinceDartSdk": "3.0" }, { @@ -3202,9 +3202,9 @@ "state": "stable", "incompatible": [], "sets": [], - "fixStatus": "needsFix", + "fixStatus": "hasFix", "details": "**AVOID** using multiple underscores when a single wildcard will do.\n\n**BAD:**\n```dart\nvoid function(int __) { }\n```\n\n**GOOD:**\n```dart\nvoid function(int _) { }\n```", - "sinceDartSdk": "3.7-wip" + "sinceDartSdk": "3.7" }, { "name": "unreachable_from_main", @@ -3259,8 +3259,8 @@ "incompatible": [], "sets": [], "fixStatus": "noFix", - "details": "Don't declare non-covariant members.\n\nAn instance variable whose type contains a type parameter of the\nenclosing class, mixin, or enum in a non-covariant position is\nlikely to cause run-time failures due to failing type\nchecks. For example, in `class C {...}`, an instance variable\nof the form `void Function(X) myVariable;` may cause this kind\nof run-time failure.\n\nThe same is true for a getter or method whose return type has a\nnon-covariant occurrence of a type parameter of the enclosing\ndeclaration.\n\nThis lint flags this kind of member declaration.\n\n**BAD:**\n```dart\nclass C {\n final bool Function(X) fun; // LINT\n C(this.fun);\n}\n\nvoid main() {\n C c = C((i) => i.isEven);\n c.fun(10); // Throws.\n}\n```\n\nThe problem is that `X` occurs as a parameter type in the type\nof `fun`. A better approach is to ensure that the non-covariant\nmember `fun` is _only_ used on `this`. We cannot strictly\nenforce this, but we can make it private and add a forwarding\nmethod `fun`:\n\n**BETTER:**\n```dart\nclass C {\n // ignore: unsafe_variance\n final bool Function(X) _fun;\n bool fun(X x) => _fun(x);\n C(this.fun);\n}\n\nvoid main() {\n C c = C((i) => i.isEven);\n c.fun(10); // Succeeds.\n}\n```\n\nA fully safe approach requires a feature that Dart does not yet\nhave, namely statically checked variance. With that, we could\nspecify that the type parameter `X` is invariant (`inout X`).\n\nAnother possibility is to declare the variable to have a safe\nbut more general type. It is then safe to use the variable\nitself, but every invocation will have to be checked at run\ntime:\n\n**HONEST:**\n```dart\nclass C {\n final bool Function(Never) fun;\n C(this.fun);\n}\n\nvoid main() {\n C c = C((i) => i.isEven);\n var cfun = c.fun; // Local variable, enables promotion.\n if (cfun is bool Function(int)) cfun(10); // Succeeds.\n if (cfun is bool Function(bool)) cfun(true); // Not called.\n}\n```", - "sinceDartSdk": "3.7-wip" + "details": "An instance variable whose type contains a type parameter of the\nenclosing class, mixin, or enum in a non-covariant position is\nlikely to cause run-time failures due to failing type\nchecks. For example, in `class C {...}`, an instance variable\nof the form `void Function(X) myVariable;` may cause this kind\nof run-time failure.\n\nThe same is true for a getter or method whose return type has a\nnon-covariant occurrence of a type parameter of the enclosing\ndeclaration.\n\nThis lint flags this kind of member declaration.\n\n**BAD:**\n```dart\nclass C {\n final bool Function([!X!]) fun; // LINT\n C(this.fun);\n}\n\nvoid main() {\n C c = C((i) => i.isEven);\n c.fun(10); // Throws.\n}\n```\n\nThe problem is that `X` occurs as a parameter type in the type\nof `fun`.\n\nOne way to reduce the potential for run-time type errors is to\nensure that the non-covariant member `fun` is _only_ used on\n`this`. We cannot strictly enforce this, but we can make it\nprivate and add a forwarding method `fun` such that we can check\nlocally in the same library that this constraint is satisfied:\n\n**BETTER:**\n```dart\nclass C {\n // ignore: unsafe_variance\n final bool Function(X) _fun;\n bool fun(X x) => _fun(x);\n C(this._fun);\n}\n\nvoid main() {\n C c = C((i) => i.isEven);\n c.fun(10); // Succeeds.\n}\n```\n\nA fully safe approach requires a feature that Dart does not yet\nhave, namely statically checked variance. With that, we could\nspecify that the type parameter `X` is invariant (`inout X`).\n\nIt is possible to emulate invariance without support for statically\nchecked variance. This puts some restrictions on the creation of\nsubtypes, but faithfully provides the typing that `inout` would\ngive:\n\n**GOOD:**\n```dart\ntypedef Inv = X Function(X);\ntypedef C = _C>;\n\nclass _C> {\n // ignore: unsafe_variance\n final bool Function(X) fun; // Safe!\n _C(this.fun);\n}\n\nvoid main() {\n C c = C((i) => i.isEven);\n c.fun(10); // Succeeds.\n}\n```\n\nWith this approach, `C` is not a subtype of `C`, so\n`c` must have a different declared type.\n\nAnother possibility is to declare the variable to have a safe\nbut more general type. It is then safe to use the variable\nitself, but every invocation will have to be checked at run\ntime:\n\n**HONEST:**\n```dart\nclass C {\n final bool Function(Never) fun;\n C(this.fun);\n}\n\nvoid main() {\n C c = C((int i) => i.isEven);\n var cfun = c.fun; // Local variable, enables promotion.\n if (cfun is bool Function(int)) cfun(10); // Succeeds.\n if (cfun is bool Function(bool)) cfun(true); // Not called.\n}\n```", + "sinceDartSdk": "3.7" }, { "name": "use_build_context_synchronously", @@ -3275,7 +3275,7 @@ "flutter" ], "fixStatus": "noFix", - "details": "**DON'T** use `BuildContext` across asynchronous gaps.\n\nStoring `BuildContext` for later usage can easily lead to difficult to diagnose\ncrashes. Asynchronous gaps are implicitly storing `BuildContext` and are some of\nthe easiest to overlook when writing code.\n\nWhen a `BuildContext` is used, a `mounted` property must be checked after an\nasynchronous gap, depending on how the `BuildContext` is accessed:\n\n* When using a `State`'s `context` property, the `State`'s `mounted` property\n must be checked.\n* For other `BuildContext` instances (like a local variable or function\n argument), the `BuildContext`'s `mounted` property must be checked.\n\n**BAD:**\n```dart\nvoid onButtonTapped(BuildContext context) async {\n await Future.delayed(const Duration(seconds: 1));\n Navigator.of(context).pop();\n}\n```\n\n**GOOD:**\n```dart\nvoid onButtonTapped(BuildContext context) {\n Navigator.of(context).pop();\n}\n```\n\n**GOOD:**\n```dart\nvoid onButtonTapped(BuildContext context) async {\n await Future.delayed(const Duration(seconds: 1));\n\n if (!context.mounted) return;\n Navigator.of(context).pop();\n}\n```\n\n**GOOD:**\n```dart\nabstract class MyState extends State {\n void foo() async {\n await Future.delayed(const Duration(seconds: 1));\n if (!mounted) return; // Checks `this.mounted`, not `context.mounted`.\n Navigator.of(context).pop();\n }\n}\n```", + "details": "**DON'T** use `BuildContext` across asynchronous gaps.\n\nStoring `BuildContext` for later usage can easily lead to difficult-to-diagnose\ncrashes. Asynchronous gaps are implicitly storing `BuildContext` and are some of\nthe easiest to overlook when writing code.\n\nWhen a `BuildContext` is used, a `mounted` property must be checked after an\nasynchronous gap, depending on how the `BuildContext` is accessed:\n\n* When using a `State`'s `context` property, the `State`'s `mounted` property\n must be checked.\n* For other `BuildContext` instances (like a local variable or function\n argument), the `BuildContext`'s `mounted` property must be checked.\n\n**BAD:**\n```dart\nvoid onButtonTapped(BuildContext context) async {\n await Future.delayed(const Duration(seconds: 1));\n Navigator.of(context).pop();\n}\n```\n\n**GOOD:**\n```dart\nvoid onButtonTapped(BuildContext context) {\n Navigator.of(context).pop();\n}\n```\n\n**GOOD:**\n```dart\nvoid onButtonTapped(BuildContext context) async {\n await Future.delayed(const Duration(seconds: 1));\n\n if (!context.mounted) return;\n Navigator.of(context).pop();\n}\n```\n\n**GOOD:**\n```dart\nabstract class MyState extends State {\n void foo() async {\n await Future.delayed(const Duration(seconds: 1));\n if (!mounted) return; // Checks `this.mounted`, not `context.mounted`.\n Navigator.of(context).pop();\n }\n}\n```", "sinceDartSdk": "2.13" }, { diff --git a/src/content/tools/diagnostic-messages.md b/src/content/tools/diagnostic-messages.md index f86ff232fd..85fa2b3cf7 100644 --- a/src/content/tools/diagnostic-messages.md +++ b/src/content/tools/diagnostic-messages.md @@ -4,6 +4,7 @@ description: Details for diagnostics produced by the Dart analyzer. body_class: highlight-diagnostics skipFreshness: true --- + {%- comment %} WARNING: Do NOT EDIT this file directly. It is autogenerated by the script in `pkg/analyzer/tool/diagnostics/generate.dart` in the sdk repository. @@ -28069,8 +28070,8 @@ comment for `f` uses a block comment style: ```dart [!/**!] -[! * Example.!] -[! */!] + [!* Example.!] + [!*/!] void f() {} ``` @@ -28690,6 +28691,36 @@ class C { } ``` +### unnecessary_ignore + +_The diagnostic '{0}' isn't produced at this location so it doesn't need to be +ignored._ + +_The diagnostic '{0}' isn't produced in this file so it doesn't need to be +ignored._ + +#### Description + +The analyzer produces this diagnostic when an ignore is specified to ignore a diagnostic that isn't produced. + +#### Example + +The following code produces this diagnostic because the `unused_local_variable` +diagnostic isn't reported at the ignored location: + +```dart +// ignore: [!unused_local_variable!] +void f() {} +``` + +#### Common fixes + +Remove the ignore comment: + +```dart +void f() {} +``` + ### unnecessary_lambdas _Closure should be a tearoff._ @@ -29219,8 +29250,8 @@ _Unnecessary use of multiple underscores._ #### Description The analyzer produces this diagnostic when an unused variable is named -with multiple underscores (for example `__`). -A single `_` wildcard variable can be used instead. +with multiple underscores (for example `__`). A single `_` wildcard variable +can be used instead. #### Example diff --git a/tool/dart_site/lib/dart_site.dart b/tool/dart_site/lib/dart_site.dart index fe292a638a..e6d8c1545d 100644 --- a/tool/dart_site/lib/dart_site.dart +++ b/tool/dart_site/lib/dart_site.dart @@ -12,6 +12,7 @@ import 'src/commands/check_links.dart'; import 'src/commands/check_site_variable.dart'; import 'src/commands/format_dart.dart'; import 'src/commands/freshness.dart'; +import 'src/commands/generate_diagnostic_docs.dart'; import 'src/commands/generate_effective_dart_toc.dart'; import 'src/commands/refresh_excerpts.dart'; import 'src/commands/serve.dart'; @@ -34,6 +35,7 @@ final class DartSiteCommandRunner extends CommandRunner { addCommand(RefreshExcerptsCommand()); addCommand(FormatDartCommand()); addCommand(FreshnessCommand()); + addCommand(GenerateDiagnosticDocs()); addCommand(GenerateEffectiveDartToc()); addCommand(AnalyzeDartCommand()); addCommand(TestDartCommand()); diff --git a/tool/dart_site/lib/src/commands/generate_diagnostic_docs.dart b/tool/dart_site/lib/src/commands/generate_diagnostic_docs.dart new file mode 100644 index 0000000000..77c5773078 --- /dev/null +++ b/tool/dart_site/lib/src/commands/generate_diagnostic_docs.dart @@ -0,0 +1,39 @@ +// 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:args/command_runner.dart'; + +import '../diagnostics/diagnostics.dart' as diagnostics; +import '../diagnostics/linter.dart' as linter; + +final class GenerateDiagnosticDocs extends Command { + GenerateDiagnosticDocs(); + + @override + String get description => 'Generate and update diagnostic docs.'; + + @override + String get name => 'generate-diagnostics'; + + @override + Future run() => _update(); +} + +Future _update() async { + print('Updating src/_data/linter_rules.json...'); + await _updateLintInfo(); + + print('Updating src/content/tool/diagnostic-messages.md...'); + await _updateDiagnosticDocs(); + + return 0; +} + +Future _updateLintInfo() async { + await linter.fetchAndUpdate(); +} + +Future _updateDiagnosticDocs() async { + await diagnostics.generate(); +} diff --git a/tool/dart_site/lib/src/diagnostics/diagnostics.dart b/tool/dart_site/lib/src/diagnostics/diagnostics.dart new file mode 100644 index 0000000000..849459b63b --- /dev/null +++ b/tool/dart_site/lib/src/diagnostics/diagnostics.dart @@ -0,0 +1,260 @@ +// Copyright (c) 2019, 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 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../utils.dart'; +import 'error_code_documentation_info.dart'; +import 'error_code_info.dart'; + +/// Generate the file `diagnostics.md` based on the documentation associated +/// with the declarations of the error codes. +Future generate() async { + final sink = File(_outputPath).openWrite(); + final messages = await Messages.retrieve(); + final generator = DocumentationGenerator(messages); + generator.writeDocumentation(sink); + await sink.flush(); + await sink.close(); +} + +/// Compute the path to the file into which documentation is being generated. +String get _outputPath => path.join( + repositoryRoot, 'src', 'content', 'tools', 'diagnostic-messages.md'); + +/// An information holder containing information about a diagnostic that was +/// extracted from the instance creation expression. +class DiagnosticInformation { + /// The name of the diagnostic. + final String name; + + /// The messages associated with the diagnostic. + final List messages; + + /// The previous names by which this diagnostic has been known. + final List previousNames = []; + + /// The documentation text associated with the diagnostic. + String? documentation; + + /// Initialize a newly created information holder with the given [name] and + /// [message]. + DiagnosticInformation(this.name, String message) : messages = [message]; + + /// Return `true` if this diagnostic has documentation. + bool get hasDocumentation => documentation != null; + + /// Add the [message] to the list of messages associated with the diagnostic. + void addMessage(String message) { + if (!messages.contains(message)) { + messages.add(message); + } + } + + void addPreviousName(String previousName) { + if (!previousNames.contains(previousName)) { + previousNames.add(previousName); + } + } + + /// Return the full documentation for this diagnostic. + void writeOn(StringSink sink) { + messages.sort(); + sink.writeln('### ${name.toLowerCase()}'); + for (final previousName in previousNames) { + sink.writeln(); + final previousInLowerCase = previousName.toLowerCase(); + sink.writeln('' + '_(Previously known as `$previousInLowerCase`)_'); + } + for (final message in messages) { + sink.writeln(); + for (final line in _split('_${_escape(message)}_')) { + sink.writeln(line); + } + } + sink.writeln(); + sink.writeln(documentation!); + } + + /// Return a version of the [text] in which characters that have special + /// meaning in markdown have been escaped. + String _escape(String text) { + return text.replaceAll('_', '\\_'); + } + + /// Split the [message] into multiple lines, each of which is less than 80 + /// characters long. + List _split(String message) { + // This uses a brute force approach because we don't expect to have messages + // that need to be split more than once. + final length = message.length; + if (length <= 80) { + return [message]; + } + final endIndex = message.lastIndexOf(' ', 80); + if (endIndex < 0) { + return [message]; + } + return [message.substring(0, endIndex), message.substring(endIndex + 1)]; + } +} + +/// A class used to generate diagnostic documentation. +class DocumentationGenerator { + /// A map from the name of a diagnostic to the information about that + /// diagnostic. + final Map infoByName = {}; + + /// Initialize a newly created documentation generator. + DocumentationGenerator(Messages messages) { + for (final classEntry in messages.analyzerMessages.entries) { + _extractAllDocs(classEntry.key, classEntry.value); + } + for (final classEntry in messages.linterMessages.entries) { + _extractAllDocs(classEntry.key, classEntry.value); + } + + _extractAllDocs('ParserErrorCode', + messages.cfeToAnalyzerErrorCodeTables.analyzerCodeToInfo); + } + + /// Writes the documentation to [sink]. + void writeDocumentation(StringSink sink) { + _writeHeader(sink); + _writeGlossary(sink); + _writeDiagnostics(sink); + } + + /// Extract documentation from all of the files containing the definitions of + /// diagnostics. + void _extractAllDocs(String className, Map messages) { + for (final errorEntry in messages.entries) { + final errorName = errorEntry.key; + final errorCodeInfo = errorEntry.value; + if (errorCodeInfo is AliasErrorCodeInfo) { + continue; + } + final name = errorCodeInfo.sharedName ?? errorName; + var info = infoByName[name]; + final message = convertTemplate( + errorCodeInfo.computePlaceholderToIndexMap(), + errorCodeInfo.problemMessage); + if (info == null) { + info = DiagnosticInformation(name, message); + infoByName[name] = info; + } else { + info.addMessage(message); + } + final previousName = errorCodeInfo.previousName; + if (previousName != null) { + info.addPreviousName(previousName); + } + final docs = _extractDoc('$className.$errorName', errorCodeInfo); + if (docs.isNotEmpty) { + if (info.documentation != null) { + throw StateError( + 'Documentation defined multiple times for ${info.name}'); + } + info.documentation = docs; + } + } + } + + /// Extract documentation from the given [errorCodeInfo]. + String _extractDoc(String errorCode, ErrorCodeInfo errorCodeInfo) { + final parsedComment = + parseErrorCodeDocumentation(errorCode, errorCodeInfo.documentation); + if (parsedComment == null) { + return ''; + } + return [ + for (final documentationPart in parsedComment) + documentationPart.formatForDocumentation() + ].join('\n'); + } + + /// Write the documentation for all of the diagnostics. + void _writeDiagnostics(StringSink sink) { + sink.write(''' + +## Diagnostics + +The analyzer produces the following diagnostics for code that +doesn't conform to the language specification or +that might work in unexpected ways. + +[bottom type]: https://dart.dev/null-safety/understanding-null-safety#top-and-bottom +[debugPrint]: https://api.flutter.dev/flutter/foundation/debugPrint.html +[ffi]: https://dart.dev/interop/c-interop +[IEEE 754]: https://en.wikipedia.org/wiki/IEEE_754 +[irrefutable pattern]: https://dart.dev/resources/glossary#irrefutable-pattern +[kDebugMode]: https://api.flutter.dev/flutter/foundation/kDebugMode-constant.html +[meta-doNotStore]: https://pub.dev/documentation/meta/latest/meta/doNotStore-constant.html +[meta-doNotSubmit]: https://pub.dev/documentation/meta/latest/meta/doNotSubmit-constant.html +[meta-factory]: https://pub.dev/documentation/meta/latest/meta/factory-constant.html +[meta-immutable]: https://pub.dev/documentation/meta/latest/meta/immutable-constant.html +[meta-internal]: https://pub.dev/documentation/meta/latest/meta/internal-constant.html +[meta-literal]: https://pub.dev/documentation/meta/latest/meta/literal-constant.html +[meta-mustBeConst]: https://pub.dev/documentation/meta/latest/meta/mustBeConst-constant.html +[meta-mustCallSuper]: https://pub.dev/documentation/meta/latest/meta/mustCallSuper-constant.html +[meta-optionalTypeArgs]: https://pub.dev/documentation/meta/latest/meta/optionalTypeArgs-constant.html +[meta-sealed]: https://pub.dev/documentation/meta/latest/meta/sealed-constant.html +[meta-useResult]: https://pub.dev/documentation/meta/latest/meta/useResult-constant.html +[meta-UseResult]: https://pub.dev/documentation/meta/latest/meta/UseResult-class.html +[meta-visibleForOverriding]: https://pub.dev/documentation/meta/latest/meta/visibleForOverriding-constant.html +[meta-visibleForTesting]: https://pub.dev/documentation/meta/latest/meta/visibleForTesting-constant.html +[package-logging]: https://pub.dev/packages/logging +[refutable pattern]: https://dart.dev/resources/glossary#refutable-pattern +'''); + final errorCodes = infoByName.keys.toList(); + errorCodes.sort(); + for (final errorCode in errorCodes) { + final info = infoByName[errorCode]!; + if (info.hasDocumentation) { + sink.writeln(); + info.writeOn(sink); + } + } + } + + /// Link to the glossary. + void _writeGlossary(StringSink sink) { + sink.write(r''' + +[constant context]: /resources/glossary#constant-context +[definite assignment]: /resources/glossary#definite-assignment +[mixin application]: /resources/glossary#mixin-application +[override inference]: /resources/glossary#override-inference +[part file]: /resources/glossary#part-file +[potentially non-nullable]: /resources/glossary#potentially-non-nullable +[public library]: /resources/glossary#public-library +'''); + } + + /// Write the header of the file. + void _writeHeader(StringSink sink) { + sink.write(''' +--- +title: Diagnostic messages +description: Details for diagnostics produced by the Dart analyzer. +body_class: highlight-diagnostics +skipFreshness: true +--- + +{%- comment %} +WARNING: Do NOT EDIT this file directly. It is autogenerated by the script in +`pkg/analyzer/tool/diagnostics/generate.dart` in the sdk repository. +Update instructions: https://github.com/dart-lang/site-www/issues/1949 +{% endcomment -%} + +This page lists diagnostic messages produced by the Dart analyzer, +with details about what those messages mean and how you can fix your code. +For more information about the analyzer, see +[Customizing static analysis](/tools/analysis). +'''); + } +} diff --git a/tool/dart_site/lib/src/diagnostics/error_code_documentation_info.dart b/tool/dart_site/lib/src/diagnostics/error_code_documentation_info.dart new file mode 100644 index 0000000000..dfe1937ae4 --- /dev/null +++ b/tool/dart_site/lib/src/diagnostics/error_code_documentation_info.dart @@ -0,0 +1,296 @@ +// Copyright (c) 2021, 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 'dart:convert'; + +/// Converts the given [documentation] string into a list of +/// [ErrorCodeDocumentationPart] objects. These objects represent +/// user-publishable documentation about the given [errorCode], along with code +/// blocks illustrating when the error occurs and how to fix it. +List? parseErrorCodeDocumentation( + String errorCode, String? documentation) { + if (documentation == null) { + return null; + } + final documentationLines = documentation.split('\n'); + if (documentationLines.isEmpty) { + return null; + } + final parser = _ErrorCodeDocumentationParser(errorCode, documentationLines); + parser.parse(); + return parser.result; +} + +/// Enum representing the different documentation sections in which an +/// [ErrorCodeDocumentationBlock] might appear. +enum BlockSection { + /// The "Examples" section, where we give examples of code that + /// generates the error. + examples, + + /// The "Common fixes" section, where we give examples of code that + /// doesn't generate the error. + commonFixes, +} + +/// An [ErrorCodeDocumentationPart] containing a block of code. +class ErrorCodeDocumentationBlock extends ErrorCodeDocumentationPart { + /// The code itself. + final String text; + + /// The section this block is contained in. + final BlockSection containingSection; + + /// The file type of this code block (e.g. `dart` or `yaml`). + final String fileType; + + /// The language version that must be active for this code to behave as + /// expected (if any). + final String? languageVersion; + + /// If this code is an auxiliary file that supports other blocks, the URI of + /// the file. + final String? uri; + + ErrorCodeDocumentationBlock(this.text, + {required this.containingSection, + required this.fileType, + this.languageVersion, + this.uri}); + + @override + String formatForDocumentation() { + return ['```$fileType', _migrateHighlightingSpans(text), '```'].join('\n'); + } +} + +/// Adjust the highlight spans in analyzer diagnostic doc code blocks +/// so that they are compatible with our static site generation. +/// +/// Analyzer diagnostic docs mark spans across lines, +/// but our tooling doesn't currently support doing so. +/// So this function adds opening or closing marks as necessary. +// TODO(parlough): Migrate analyzer docs away from cross-line spans. +String _migrateHighlightingSpans(String input) { + const openingMark = '[!'; + const closingMark = '!]'; + final lines = const LineSplitter().convert(input); + var isOpen = false; + final resultLines = []; + + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + final openIndex = line.indexOf(openingMark); + final closeIndex = line.indexOf(closingMark); + final lineNumber = i + 1; + + if (openIndex == -1 && closeIndex == -1) { + if (isOpen) { + final trimmedLine = line.trimLeft(); + if (trimmedLine.isNotEmpty) { + final leadingSpaceCount = line.length - trimmedLine.length; + resultLines.add( + '${' ' * leadingSpaceCount}$openingMark$trimmedLine$closingMark'); + } else { + resultLines.add(line); + } + } else { + resultLines.add(line); + } + continue; + } + + if (openIndex == -1) { + if (!isOpen) { + throw StateError('Unexpected closing tag at line $lineNumber: $line'); + } + final trimmedLine = line.trimLeft(); + final leadingSpaceCount = + trimmedLine.isNotEmpty ? line.length - trimmedLine.length : 0; + + resultLines.add('${' ' * leadingSpaceCount}$openingMark' + '${line.substring(leadingSpaceCount, closeIndex)}' + '$closingMark' + '${line.substring(closeIndex + closingMark.length)}'); + isOpen = false; + continue; + } + + if (closeIndex == -1) { + if (isOpen) { + throw StateError('Overlapping span at line $lineNumber: $line'); + } + resultLines.add('${line.substring(0, openIndex)}$openingMark' + '${line.substring(openIndex + openingMark.length)}$closingMark'); + isOpen = true; + continue; + } + + if (isOpen) { + if (openIndex < closeIndex) { + throw StateError('Overlapping span at line $lineNumber: $line'); + } + resultLines.add('$openingMark${line.substring(0, closeIndex)}$closingMark' + '${line.substring(closeIndex + closingMark.length)}'); + isOpen = false; + } else { + if (closeIndex < openIndex) { + throw StateError('Unexpected closing tag at line $lineNumber: $line'); + } + resultLines.add(line); + } + } + + if (isOpen) { + resultLines.add(closingMark); + } + + if (resultLines.lastOrNull?.trim().isEmpty ?? false) { + resultLines.removeLast(); + } + + return resultLines.join('\n'); +} + +/// A portion of an error code's documentation. This could be free form +/// markdown text ([ErrorCodeDocumentationText]) or a code block +/// ([ErrorCodeDocumentationBlock]). +abstract class ErrorCodeDocumentationPart { + /// Formats this documentation part as text suitable for inclusion in the + /// analyzer's `diagnostics.md` file. + String formatForDocumentation(); +} + +/// An [ErrorCodeDocumentationPart] containing free form markdown text. +class ErrorCodeDocumentationText extends ErrorCodeDocumentationPart { + /// The text, in markdown format. + final String text; + + ErrorCodeDocumentationText(this.text); + + @override + String formatForDocumentation() => text; +} + +class _ErrorCodeDocumentationParser { + /// The prefix used on directive lines to specify the language version for + /// the snippet. + static const String _languagePrefix = '%language='; + + /// The prefix used on directive lines to indicate the URI of an auxiliary + /// file that is needed for testing purposes. + static const String _uriDirectivePrefix = '%uri="'; + + final String errorCode; + + final List commentLines; + + final List result = []; + + int currentLineNumber = 0; + + String? currentSection; + + _ErrorCodeDocumentationParser(this.errorCode, this.commentLines); + + bool get done => currentLineNumber >= commentLines.length; + + String get line => commentLines[currentLineNumber]; + + BlockSection? computeCurrentBlockSection() { + return switch (currentSection) { + '#### Example' || '#### Examples' => BlockSection.examples, + '#### Common fixes' => BlockSection.commonFixes, + null => problem('Code block before section header'), + _ => null, + }; + } + + void parse() { + var textLines = []; + + void flushText() { + if (textLines.isNotEmpty) { + result.add(ErrorCodeDocumentationText(textLines.join('\n'))); + textLines = []; + } + } + + while (!done) { + if (line.startsWith('TODO')) { + // Everything after the "TODO" is ignored. + break; + } else if (line.startsWith('%')) { + problem('% directive outside code block'); + } else if (line.startsWith('```')) { + flushText(); + processCodeBlock(); + } else { + if (line.startsWith('#') && !line.startsWith('#####')) { + currentSection = line; + } + textLines.add(line); + currentLineNumber++; + } + } + flushText(); + } + + Never problem(String explanation) { + throw FormatException('In documentation for $errorCode, ' + 'at line ${currentLineNumber + 1}, $explanation'); + } + + void processCodeBlock() { + final containingSection = computeCurrentBlockSection(); + final codeLines = []; + String? languageVersion; + String? uri; + assert(line.startsWith('```')); + final fileType = line.substring(3); + if (fileType.isEmpty && containingSection != null) { + problem('Code blocks should have a file type, e.g. "```dart"'); + } + ++currentLineNumber; + while (true) { + if (done) { + problem('Unterminated code block'); + } else if (line.startsWith('```')) { + if (line != '```') { + problem('Code blocks should end with "```"'); + } + ++currentLineNumber; + if (containingSection != null) { + // Ignore code blocks where they're allowed but aren't checked. + result.add(ErrorCodeDocumentationBlock(codeLines.join('\n'), + containingSection: containingSection, + fileType: fileType, + languageVersion: languageVersion, + uri: uri)); + } + return; + } else if (line.startsWith('%')) { + if (line.startsWith(_languagePrefix)) { + if (languageVersion != null) { + problem('Multiple language version directives'); + } + languageVersion = line.substring(_languagePrefix.length); + } else if (line.startsWith(_uriDirectivePrefix)) { + if (uri != null) { + problem('Multiple URI directives'); + } + if (!line.endsWith('"')) { + problem('URI directive should be surrounded by double quotes'); + } + uri = line.substring(_uriDirectivePrefix.length, line.length - 1); + } else { + problem('Unrecognized directive ${json.encode(line)}'); + } + } else { + codeLines.add(line); + } + ++currentLineNumber; + } + } +} diff --git a/tool/dart_site/lib/src/diagnostics/error_code_info.dart b/tool/dart_site/lib/src/diagnostics/error_code_info.dart new file mode 100644 index 0000000000..cc564d79fe --- /dev/null +++ b/tool/dart_site/lib/src/diagnostics/error_code_info.dart @@ -0,0 +1,345 @@ +// Copyright (c) 2021, 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 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:yaml/yaml.dart' show loadYaml; + +final class Messages { + /// Decoded messages from the analyzer's `messages.yaml` file. + final Map> analyzerMessages; + + /// Decoded messages from the linter's `messages.yaml` file. + final Map> linterMessages; + + /// Decoded messages from the front end's `messages.yaml` file. + final Map frontEndMessages; + + /// A set of tables mapping between front end and analyzer error codes. + final CfeToAnalyzerErrorCodeTables cfeToAnalyzerErrorCodeTables; + + Messages._({ + required this.analyzerMessages, + required this.linterMessages, + required this.frontEndMessages, + required this.cfeToAnalyzerErrorCodeTables, + }); + + static Future retrieve() async { + final rawAnalyzerYaml = await _loadSdkYaml('pkg/analyzer/messages.yaml'); + final analyzerMessages = _decodeAnalyzerMessagesYaml(rawAnalyzerYaml); + final rawFrontEndYaml = await _loadSdkYaml('pkg/front_end/messages.yaml'); + final frontEndMessages = _decodeCfeMessagesYaml(rawFrontEndYaml); + final rawLinterYaml = await _loadSdkYaml('pkg/linter/messages.yaml'); + final linterMessages = _decodeAnalyzerMessagesYaml(rawLinterYaml); + + return Messages._( + analyzerMessages: analyzerMessages, + linterMessages: linterMessages, + frontEndMessages: frontEndMessages, + cfeToAnalyzerErrorCodeTables: + CfeToAnalyzerErrorCodeTables._(frontEndMessages), + ); + } +} + +/// Pattern used by the front end to identify placeholders in error message +/// strings. +final RegExp _placeholderPattern = + RegExp('#([-a-zA-Z0-9_]+)(?:%([0-9]*).([0-9]+))?'); + +/// Convert a CFE template string (which uses placeholders like `#string`) to +/// an analyzer template string (which uses placeholders like `{0}`). +String convertTemplate(Map placeholderToIndexMap, String entry) { + return entry.replaceAllMapped(_placeholderPattern, + (match) => '{${placeholderToIndexMap[match.group(0)!]}}'); +} + +Future _fetchSdkFile(String pathInSdk) async { + final uri = Uri.parse( + 'https://raw.githubusercontent.com/dart-lang/sdk/refs/heads/main/$pathInSdk'); + final rawFile = await http.read(uri); + return rawFile; +} + +Future _loadSdkYaml(String pathInSdk) async { + final yamlString = await _fetchSdkFile(pathInSdk); + return loadYaml(yamlString) as Object?; +} + +/// Decodes a YAML object (obtained from a `messages.yaml` file) into a +/// two-level map of [ErrorCodeInfo], indexed first by class name and then by +/// error name. +Map> _decodeAnalyzerMessagesYaml( + Object? yaml) { + Never problem(String message) { + throw Exception('Problem in analyzer/messages.yaml: $message'); + } + + final result = >{}; + if (yaml is! Map) { + problem('root node is not a map'); + } + for (final classEntry in yaml.entries) { + final className = classEntry.key; + if (className is! String) { + problem('non-string class key ${json.encode(className)}'); + } + final classValue = classEntry.value; + if (classValue is! Map) { + problem('value associated with class key $className is not a map'); + } + for (final errorEntry in classValue.entries) { + final errorName = errorEntry.key; + if (errorName is! String) { + problem('in class $className, non-string error key ' + '${json.encode(errorName)}'); + } + final errorValue = errorEntry.value; + if (errorValue is! Map) { + problem('value associated with error $className.$errorName is not a ' + 'map'); + } + + AnalyzerErrorCodeInfo errorCodeInfo; + try { + errorCodeInfo = (result[className] ??= {})[errorName] = + AnalyzerErrorCodeInfo.fromYaml(errorValue); + } catch (e, st) { + Error.throwWithStackTrace( + 'while processing $className.$errorName, $e', st); + } + + if (errorCodeInfo case AliasErrorCodeInfo(:final aliasFor)) { + final aliasForPath = aliasFor.split('.'); + if (aliasForPath.isEmpty) { + problem("The 'aliasFor' value at '$className.$errorName is empty"); + } + var node = yaml; + for (final key in aliasForPath) { + final value = node[key]; + if (value is! Map) { + problem('No Map value at "$aliasFor", aliased from ' + '$className.$errorName'); + } + node = value; + } + } + } + } + return result; +} + +/// Decodes a YAML object (obtained from `pkg/front_end/messages.yaml`) into a +/// map from error name to [ErrorCodeInfo]. +Map _decodeCfeMessagesYaml(Object? yaml) { + Never problem(String message) { + throw Exception('Problem in pkg/front_end/messages.yaml: $message'); + } + + final result = {}; + if (yaml is! Map) { + problem('root node is not a map'); + } + for (final entry in yaml.entries) { + final errorName = entry.key; + if (errorName is! String) { + problem('non-string error key ${json.encode(errorName)}'); + } + final errorValue = entry.value; + if (errorValue is! Map) { + problem('value associated with error $errorName is not a map'); + } + result[errorName] = FrontEndErrorCodeInfo.fromYaml(errorValue); + } + return result; +} + +/// An [AnalyzerErrorCodeInfo] which is an alias for another, for incremental +/// deprecation purposes. +class AliasErrorCodeInfo extends AnalyzerErrorCodeInfo { + final String aliasFor; + + AliasErrorCodeInfo._fromYaml(super.yaml, {required this.aliasFor}) + : super._fromYaml(); + + String get aliasForClass => aliasFor.split('.').first; +} + +/// In-memory representation of error code information obtained from the +/// analyzer's `messages.yaml` file. +class AnalyzerErrorCodeInfo extends ErrorCodeInfo { + AnalyzerErrorCodeInfo({ + super.correctionMessage, + super.deprecatedMessage, + super.documentation, + required super.problemMessage, + super.sharedName, + }); + + factory AnalyzerErrorCodeInfo.fromYaml(Map yaml) { + if (yaml['aliasFor'] case final aliasFor?) { + return AliasErrorCodeInfo._fromYaml(yaml, aliasFor: aliasFor as String); + } else { + return AnalyzerErrorCodeInfo._fromYaml(yaml); + } + } + + AnalyzerErrorCodeInfo._fromYaml(super.yaml) : super.fromYaml(); +} + +/// Data tables mapping between CFE errors and their corresponding automatically +/// generated analyzer errors. +class CfeToAnalyzerErrorCodeTables { + /// List of CFE errors for which analyzer errors should be automatically + /// generated, organized by their `index` property. + final List indexToInfo = []; + + /// Map whose values are the CFE errors for which analyzer errors should be + /// automatically generated, and whose keys are the corresponding analyzer + /// error name. (Names are simple identifiers; they are not prefixed by the + /// class name `ParserErrorCode`) + final Map analyzerCodeToInfo = {}; + + CfeToAnalyzerErrorCodeTables._(Map messages) { + for (final entry in messages.entries) { + final errorCodeInfo = entry.value; + final index = errorCodeInfo.index; + if (index == null || errorCodeInfo.analyzerCode.length != 1) { + continue; + } + final frontEndCode = entry.key; + if (index < 1) { + throw Exception(''' +$frontEndCode specifies index $index but indices must be 1 or greater. +For more information run: +pkg/front_end/tool/fasta generate-messages +'''); + } + if (indexToInfo.length <= index) { + indexToInfo.length = index + 1; + } + final previousEntryForIndex = indexToInfo[index]; + if (previousEntryForIndex != null) { + throw Exception('Index $index used by both ' + '$previousEntryForIndex and $frontEndCode'); + } + indexToInfo[index] = errorCodeInfo; + final analyzerCodeLong = errorCodeInfo.analyzerCode.single; + final expectedPrefix = 'ParserErrorCode.'; + if (!analyzerCodeLong.startsWith(expectedPrefix)) { + throw Exception('Expected all analyzer error codes to be prefixed with ' + '${json.encode(expectedPrefix)}. Found ' + '${json.encode(analyzerCodeLong)}.'); + } + final analyzerCode = analyzerCodeLong.substring(expectedPrefix.length); + final previousEntryForAnalyzerCode = analyzerCodeToInfo[analyzerCode]; + if (previousEntryForAnalyzerCode != null) { + throw Exception('Analyzer code $analyzerCode used by both ' + '$previousEntryForAnalyzerCode and ' + '$frontEndCode'); + } + analyzerCodeToInfo[analyzerCode] = errorCodeInfo; + } + for (var i = 1; i < indexToInfo.length; i++) { + if (indexToInfo[i] == null) { + throw Exception( + 'Indices are not consecutive; no error code has index $i.'); + } + } + } +} + +/// In-memory representation of error code information obtained from either the +/// analyzer or the front end's `messages.yaml` file. This class contains the +/// common functionality supported by both formats. +abstract class ErrorCodeInfo { + /// If the error code has an associated correctionMessage, the template for + /// it. + final String? correctionMessage; + + /// If non-null, the deprecation message for this error code. + final String? deprecatedMessage; + + /// If present, user-facing documentation for the error. + final String? documentation; + + /// The problemMessage for the error code. + final String problemMessage; + + /// If present, indicates that this error code has a special name for + /// presentation to the user, that is potentially shared with other error + /// codes. + final String? sharedName; + + /// If present, indicates that this error code has been renamed from + /// [previousName] to its current name (or [sharedName]). + final String? previousName; + + ErrorCodeInfo({ + this.documentation, + this.sharedName, + required this.problemMessage, + this.correctionMessage, + this.deprecatedMessage, + this.previousName, + }); + + /// Decodes an [ErrorCodeInfo] object from its YAML representation. + ErrorCodeInfo.fromYaml(Map yaml) + : this( + correctionMessage: yaml['correctionMessage'] as String?, + deprecatedMessage: yaml['deprecatedMessage'] as String?, + documentation: yaml['documentation'] as String?, + problemMessage: yaml['problemMessage'] as String? ?? '', + sharedName: yaml['sharedName'] as String?, + previousName: yaml['previousName'] as String?); + + /// Given a messages.yaml entry, come up with a mapping from placeholder + /// patterns in its message strings to their corresponding indices. + Map computePlaceholderToIndexMap() { + final mapping = {}; + for (final value in [problemMessage, correctionMessage]) { + if (value is! String) continue; + for (final Match match in _placeholderPattern.allMatches(value)) { + // CFE supports a bunch of formatting options that analyzer doesn't; + // make sure none of those are used. + if (match.group(0) != '#${match.group(1)}') { + throw Exception( + 'Template string ${json.encode(value)} contains unsupported ' + 'placeholder pattern ${json.encode(match.group(0))}'); + } + + mapping[match.group(0)!] ??= mapping.length; + } + } + return mapping; + } +} + +/// In-memory representation of error code information obtained from the front +/// end's `messages.yaml` file. +class FrontEndErrorCodeInfo extends ErrorCodeInfo { + /// The set of analyzer error codes that corresponds to this error code, if + /// any. + final List analyzerCode; + + /// The index of the error in the analyzer's `fastaAnalyzerErrorCodes` table. + final int? index; + + FrontEndErrorCodeInfo.fromYaml(super.yaml) + : analyzerCode = _decodeAnalyzerCode(yaml['analyzerCode']), + index = yaml['index'] as int?, + super.fromYaml(); + + static List _decodeAnalyzerCode(Object? value) { + return switch (value) { + null => const [], + String() => [value], + List() => [for (final s in value) s as String], + _ => throw Exception('Unrecognized analyzer code: $value'), + }; + } +} diff --git a/tool/dart_site/lib/src/diagnostics/linter.dart b/tool/dart_site/lib/src/diagnostics/linter.dart new file mode 100644 index 0000000000..bdc658beb7 --- /dev/null +++ b/tool/dart_site/lib/src/diagnostics/linter.dart @@ -0,0 +1,57 @@ +// 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 'dart:convert'; +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart' show loadYaml; + +import '../utils.dart'; + +String get _outputPath => + path.join(repositoryRoot, 'src', '_data', 'linter_rules.json'); + +Future fetchAndUpdate() async { + final rawRulesInfoUri = Uri.parse( + 'https://raw.githubusercontent.com/dart-lang/sdk/refs/heads/main/pkg/linter/tool/machine/rules.json'); + final rawRulesInfo = await http.read(rawRulesInfoUri); + final rulesInfo = + (jsonDecode(rawRulesInfo) as List).cast>(); + + final coreRules = await _rulesConfigured( + 'https://raw.githubusercontent.com/dart-lang/core/refs/heads/main/pkgs/lints/lib/core.yaml'); + final recommendedRules = await _rulesConfigured( + 'https://raw.githubusercontent.com/dart-lang/core/refs/heads/main/pkgs/lints/lib/recommended.yaml'); + final flutterRules = await _rulesConfigured( + 'https://raw.githubusercontent.com/flutter/packages/refs/heads/main/packages/flutter_lints/lib/flutter.yaml'); + + for (final rule in rulesInfo) { + final ruleName = rule['name'] as String; + rule['sets'] = { + if (coreRules.contains(ruleName)) ...['core', 'recommended', 'flutter'], + if (recommendedRules.contains(ruleName)) ...['recommended', 'flutter'], + if (flutterRules.contains(ruleName)) 'flutter', + }.toList(growable: false); + } + + final formattedRuleInfo = + const JsonEncoder.withIndent(' ').convert(rulesInfo); + + File(_outputPath).writeAsStringSync(formattedRuleInfo); +} + +Future> _rulesConfigured(String path) async { + final optionsUri = Uri.parse(path); + final optionsString = await http.read(optionsUri); + final options = loadYaml(optionsString) as Map; + + // Assume the structure of the analysis options file. + final linterOptions = options['linter'] as Map; + final enabledRules = linterOptions['rules'] as List; + return { + for (final ruleName in enabledRules) ruleName as String, + }; +} diff --git a/tool/dart_site/lib/src/utils.dart b/tool/dart_site/lib/src/utils.dart index c4e8f3597e..d605ea95c4 100644 --- a/tool/dart_site/lib/src/utils.dart +++ b/tool/dart_site/lib/src/utils.dart @@ -3,10 +3,22 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:io'; +import 'dart:isolate'; import 'package:args/args.dart'; import 'package:path/path.dart' as path; +/// The root path of the website repository. +String get repositoryRoot { + final packageConfigPath = path.fromUri(Isolate.packageConfigSync); + final maybeRoot = path.dirname(path.dirname(packageConfigPath)); + if (!File(path.join(maybeRoot, 'dash_site')).existsSync()) { + throw StateError('Trying calling dash_site from the root directory.'); + } + + return maybeRoot; +} + final bool _runningInCi = Platform.environment['CI'] == 'true'; void groupStart(String text) { diff --git a/tool/dart_site/pubspec.yaml b/tool/dart_site/pubspec.yaml index 4c0a868bb7..5a8a9e110b 100644 --- a/tool/dart_site/pubspec.yaml +++ b/tool/dart_site/pubspec.yaml @@ -12,11 +12,13 @@ dependencies: path: ../../site-shared/pkgs/excerpter fbh_front_matter: ^0.0.1 html_unescape: ^2.0.0 + http: ^1.3.0 intl: ^0.20.0 io: ^1.0.5 linkcheck: ^3.0.0 markdown: ^7.2.2 path: ^1.9.0 + yaml: ^3.1.3 yaml_variable_scanner: ^0.0.5 dev_dependencies: