Adding a new diagnostic

This document describes the process of adding a new (non-lint) diagnostic to the analyzer.

Define the diagnostic code

The first step is to define the code(s) associated with the diagnostic.

The codes are defined in the file analyzer/messages.yaml. There's a comment at the top of the file describing the structure of the file.

Every diagnostic has at least one code associated with it. A code defines the problem and correction messages that will be shown to users.

For most diagnostics, a single message (code) is sufficient. But sometimes it's useful to tailor the message based on the context in which the problem occurs or because the message can be made more clear.

For example, it‘s an error to declare two or more constructors with the same name. That’s true whether the name is explicit or implicit (the default constructor). In order for the message to match the user's model of the language, we define two messages for this one problem: one that refers to the constructors by their name, and one that refers to the constructors as “unnamed”. The way we define two messages is by defining two codes.

Each code has a unique name (the key used in the map in messages.yaml) and can optionally define a shared name that links all the codes for a single diagnostic together. It is the shared name that is displayed to users. If a shared name isn't explicitly provided, it will default to being the same as the unique name.

Updating generated files

After every edit to the messages.yaml file, you will need to run the following to update the generated files:

dart run analyzer/tool/messages/generate.dart

Add code to error_fix_status.yaml

You also need to manually add the name of the code to the list of codes in the file analysis_server/lib/src/services/correction/error_fix_status.yaml. The code should have the line

  status: needsEvaluation

nested under the name of the code.

At some point, we‘ll evaluate the diagnostics that are marked as needsEvaluation. If we can think of a reasonable and useful fix, then we’ll change it to needsFix with a comment indicating what fix we thought of. If we can‘t think of one, then we’ll change it to noFix. The status is changed to hasFix when there's at least one fix associated with the diagnostic, which is something we can (and do) verify in a test.

Write tests

We recommend writing the tests for a diagnostic before writing the code to generate the diagnostic. Doing so helps you think about the specific cases that the implementation code needs to handle, which can result in cleaner implementation code and fewer bugs.

The tests for each diagnostic code (or set of codes that have the same shared name) are in a separate file in the directory analyzer/test/src/diagnostics.

Looking at the implementation of tests in a few of the other files can help you see the basic pattern, but all the tests essentially work by setting up the code to be analyzed, then assert that either the expected diagnostic has been produced in the expected locations or that there are no diagnostics being generated. (It‘s often valuable to test that the diagnostic doesn’t have any false positives.)

Report the diagnostic

The last step is to write the code to report the diagnostic. Where that code lives depends on the kind of diagnostic you're adding.

If you‘re adding a diagnostic that’s defined by the language specification (with a severity of ‘error’), then the best place to implement it will usually be in one of the <node class>Resolver classes.

If you're adding a warning, then the class BestPracticesVerifier is usually the best place for it.

Document the diagnostic

After the diagnostic has been implemented and committed, documentation for the diagnostic needs to be written.

Note: We are in the process of defining a process for this task, so this section is currently incomplete. Contact us for details if you want to write the documentation yourself. If you don‘t want to write the documentation then we’ll write it for you.

Before you start writing documentation we recommend that you look at several of the existing examples of documentation to get a feeling for the general writing style and format of the docs. Some of this information is also found below. If you have questions about style, we follow the general Google style guidelines.

The documentation for the diagnostics that are implemented in the analyzer is in the file analyzer/messages.yaml. They are under the key documentation:, and are mostly standard markdown. The differences are described below.

Template

The good news is that the documentation is highly stylized, so writing it is usually fairly easy.

To start writing the documentation, copy the following template. Each section is discussed below.

#### Description

The analyzer produces this diagnostic when

#### Example

The following code produces this diagnostic because :

```dart
```

#### Common fixes

```dart
```

Description

The Description section should start by explaining when the diagnostic will be produced. Specifically, that means the conditions that cause the diagnostic to be produced. The goal is to help the user understand why the diagnostic is appearing in their code, so the explanation needs to cover all of the possible reasons.

For example, the diagnostic invalid_extension_argument_count describes the conditions that cause the diagnostic this way:

The analyzer produces this diagnostic when an extension override doesn't have exactly one argument.

Unless it‘s fairly obvious, the description should also explain why the condition is being reported. In most cases the reason for the diagnostic will be obvious from the description of when it’s reported, but sometimes that isn't enough.

For example, the user might not be familiar with extension overrides, so the explanation above might not be sufficient. By explaining why an override must have a single argument we can help the user learn about the feature, so the documentation goes on to explain that:

The argument is the expression used to compute the value of this within the extension method, so there must be one argument.

Examples

The Examples section should show at least one example of code that will produce the diagnostic. (If there's only one example, then the title should be singular as in the template above, but if there are multiple examples the title should be plural.)

The examples should be complete in the sense that the user should be able to copy the example, paste it into an empty compilation unit, and see exactly the one diagnostic being reported. (One technique we often use for this purpose is to define local variables as parameters to a method or function so that the unused_local_variable diagnostic isn't also reported.)

The examples should be minimal so that users aren‘t distracted by irrelevant details. They should only include code that is required in order to generate the diagnostic. They should use simple names, like A, B, and C for classes, M for mixins, f and g for functions, and so on. It’s better to not use names with semantic value.

Each example must have the range of characters that are highlighted by the diagnostic enclosed between [! and !] delimiters. These are used to highlight the region on the web page and are also validated by tests.

Every code block must specify a file type, such as dart or yaml.

The examples occasionally need some additional support, which is provided by “directives”. The directives must be on the first lines of the code block, and have the form %directiveName=. There are two directives defined.

The experiments directive (%experiments=) specifies a comma-separated list of the names of language experiments that are to be enabled when analyzing the example. This is necessary when writing documentation for experiments that haven't yet shipped. These directives should be removed once the experiment is enabled by default.

The uri directive (%uri=) specifies the URI for the file containing the code block. Without a uri directive the standard uri will be used. This directive is necessary for cases when an example needs an auxiliary file to be defined, usually so that it can be imported in the example. Code blocks that have a uri directive are considered to be auxiliary files, not examples, and aren't analyzed or required to have highlight range markers. Auxiliary files exist when either an example or a common fix is analyzed.

Common fixes

The Common fixes section should show examples of the ways to fix the problem that are most likely to be applicable. In some cases there‘s only one likely way to fix the problem (like removing the null check operator when the expression isn’t nullable), but in other cases there might be multiple possible ways of fixing the problem.

There should minimally be one fix shown for every action suggested by the diagnostic's correction message.

Each fix should use one of the examples as the base and show how the invalid code can be changed to apply the described fix. The same example can be used by multiple fixes.

Each fix should be introduced by a sentence that explains what change the user would make for the fix. Usually the description is written in the form “If these conditions hold, then make this change:” so that users can tell when a suggested fix can be applied.

Fixes can't have highlight markers, and are expected to not have any diagnostics reported.