Creating a new diagnostic in quick-lint-js
A common task when contributing to quick-lint-js is to create a new diagnostic. In quick-lint-js, diagnostic is a warning or error reported while parsing the user's JavaScript code.
Creating a diagnostic involves four pieces of code:
- Diagnostic type and metadata
- Test for the diagnostic
- Detection and reporting of the diagnostic
- Documentation for the website
1. Diagnostic type and metadata
Diagnostic types are listed in
src/quick-lint-js/diag/diagnostic-types-2.h
. They look like this:
Each diagnostic type is a C++
struct
with some custom C++ [[attributes]].
The C++ compiler ignores the attributes and just sees a class:
The custom attributes are processed by a separate tool as part of quick-lint-js's build system. We will discuss the custom attributes shortly.
Let's pick a name for our
diagnostic. The name needs to start with
Diag_
and be a legal C++ class name. We will
use this name later to report the diagnostic and to test for the
diagnostic:
Each diagnostic in quick-lint-js has a unique diagnostic code as the
first argument to
[[qljs::diag]]
. A diagnostic code is the
letter E
followed by four decimal digits. Let's be lazy
and reuse an existing diagnostic code for now:
We also need to pick a diagnostic severity, either
Diagnostic_Severity::error
or
Diagnostic_Severity::warning
, as the
second argument to
[[qljs::diag]]
. Our new diagnostic is for a
possible issue, so let's pick
warning
:
Each diagnostic is a class (struct
). The
class needs to store at least one
Source_Code_Span
member variable so the
editor knows where to put the
squigglies. We should think about
where the squigglies should go and name our member variable
appropriately to make the reporting and testing code easier to read.
Since our diagnostic is about string comparisons, let's name the
member variable comparison_operator
. We write
the member variables after
the C++ attributes:
Each diagnostic needs a message specified by one or more
[[qljs::message]]
attributes. Most diagnostics are simple and just have a simple string.
Other diagnostics might have formatting or multiple strings. Our
diagnostic is simple, so let's just write a single string with no
formatting. Don't forget to mention the
Source_Code_Span
member variable we defined
inside our Diag_
class:
After adding the diagnostic type to diagnostic-types-2.h
,
build quick-lint-js. You should get a build error like the following:
A build check is telling us that the error code we chose
(E0001
) is already in use. Let's change our
Diag_
class in
diagnostic-types-2.h
to use the unused diagnostic code
suggested by the check:
Now let's build quick-lint-js again and run the tests. We should get no failures, which means we didn't break anything:
The build scripts modified a few files for us. Make sure you include these files in your commit:
Now that we have created the diagnostic type, let's move on to writing a test.
2. Test for the diagnostic
All diagnostics must be tested with an automated test. To create a
test, copy-paste an existing test in a
test/test-parse-*.cpp
file and tweak it. Let's put our
test in
test/test-parse-warning.cpp
:
There are a few pieces of this test worth mentioning:
-
test_parse_and_visit_expression
-
quick-lint-js' parser can parse several things, including
statements, expressions, and TypeScript types. Our diagnostic is
specific to JavaScript expressions, so we call
test_parse_and_visit_expression
. -
u8"a == ''"_sv
-
The input source code we want to test. The
u8
prefix is required so the code is parsed as UTF-8. The_sv
suffix is required so that code containing null bytes is handled correctly. -
^^^
-
We need to tell
test_parse_and_visit_expression
where in the source code the diagnostic should be reported. This is represented using two parts inside a string: alignment (spaces) and a span (one or more^
s, or one`
). In our example, there are two leading spaces, so the diagnostic should start at the third character (byte offset 2). The span is three characters wide (^^^
), so the diagnostic covers three characters (up until byte offset 5). The alignment and span specify that the diagnostic should cover offsets 2, 3, and 4: -
Diag_Comparison_With_Empty_String
-
We need to tell
test_parse_and_visit_expression
which kind of diagnostic we expect. We do this by writing the diagnostic class's name after the span.If a diagnostic has multiple members (such as if the diagnostic has multiple messages), the member name must appear after the diagnostic class's name. See the documentation for
_diag
and NOTE[_diag-syntax] for details.
Build and run the test to make sure it fails. The failure says that we expected a diagnostic, but didn't get any. This makes sense because we haven't written the code to report the diagnostic yet:
3. Detection and reporting of the diagnostic
Now for the hard part: writing the production code. Most likely we will report our diagnostic in one of these files:
- src/quick-lint-js/fe/lex.cpp
- src/quick-lint-js/fe/parse.cpp or parse-*.cpp
- src/quick-lint-js/fe/variable-analyzer.cpp
But these files contain thousands of lines of code. How do we know where to put our new code?
One technique is to step through the code in a debugger:
error_on_pointless_compare_against_literal
looks like a good place to put our code.
Detecting when to report the diagnostic is up to you. But once you have the information you need, reporting a diagnostics is easy:
Build and test to prove that our code worked:
Huzzah! 🥳
But we're not done yet... We still have to write 💀 documentation 💀
4. Documentation for the website
Each diagnostic has associated documentation stored separately from the code. The docs are stored in docs/errors/ with one file per diagnostic. Let's write our documentation:
Some important parts of diagnostic documentation:
- title
-
The title of the document should include the diagnostic's code. The
diagnostic's message should follow the code. The title should
include the message mentioned in
diagnostic-types-2.h
, but it doesn't have to match exactly. Interpolation markers such as{1:headlinese}
should be omitted. - first example
-
The first code snippet should be fenced with
```javascript
or```typescript
(or another other support language). This code snippet demonstrates broken code and must cause quick-lint-js to report a diagnostic. A broken code snippet is required. - second example
- The second code snippet should also be fenced. This code snippet demonstrates working code and must not cause quick-lint-js to report any diagnostic. A working code snippet is required.
- extra examples
- You can include more code snippets after the second. Each of these extra code snippet must cause no diagnostics. Usually these code snippets show alternate ways of addressing the original issue. These extra examples are optional.