blob: 72998407a76230a0f5731980df125b26528e9e27 [file] [log] [blame]
// 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:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_utilities/package_root.dart' as package_root;
import '../messages/error_code_documentation_info.dart';
import '../messages/error_code_info.dart';
/// Generate the file `diagnostics.md` based on the documentation associated
/// with the declarations of the error codes.
void main() async {
var sink = File(computeOutputPath()).openWrite();
var generator = DocumentationGenerator();
generator.writeDocumentation(sink);
await sink.flush();
await sink.close();
}
/// Compute the path to the file into which documentation is being generated.
String computeOutputPath() {
var pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
var packageRoot = pathContext.normalize(package_root.packageRoot);
var analyzerPath = pathContext.join(packageRoot, 'analyzer');
return pathContext.join(
analyzerPath, 'tool', 'diagnostics', 'diagnostics.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.
List<String> messages;
/// The previous names by which this diagnostic has been known.
List<String> 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 (var previousName in previousNames) {
sink.writeln();
var previousInLowerCase = previousName.toLowerCase();
sink.writeln('<a id="$previousInLowerCase" aria-hidden="true"></a>'
'_(Previously known as `$previousInLowerCase`)_');
}
for (String message in messages) {
sink.writeln();
for (String 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<String> _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.
int length = message.length;
if (length <= 80) {
return [message];
}
int 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.
Map<String, DiagnosticInformation> infoByName = {};
/// Initialize a newly created documentation generator.
DocumentationGenerator() {
for (var classEntry in analyzerMessages.entries) {
_extractAllDocs(classEntry.key, classEntry.value);
}
for (var errorClass in errorClasses) {
if (errorClass.includeCfeMessages) {
_extractAllDocs(
errorClass.name, cfeToAnalyzerErrorCodeTables.analyzerCodeToInfo);
// Note: only one error class has the `includeCfeMessages` flag set;
// verify_diagnostics_test.dart verifies this. So we can safely break.
break;
}
}
}
/// Write the documentation to the file at the given [outputPath].
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<String, ErrorCodeInfo> messages) {
for (var errorEntry in messages.entries) {
var errorName = errorEntry.key;
var errorCodeInfo = errorEntry.value;
var name = errorCodeInfo.sharedName ?? errorName;
var info = infoByName[name];
var message = convertTemplate(
errorCodeInfo.computePlaceholderToIndexMap(),
errorCodeInfo.problemMessage);
if (info == null) {
info = DiagnosticInformation(name, message);
infoByName[name] = info;
} else {
info.addMessage(message);
}
var previousName = errorCodeInfo.previousName;
if (previousName != null) {
info.addPreviousName(previousName);
}
var 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) {
var parsedComment =
parseErrorCodeDocumentation(errorCode, errorCodeInfo.documentation);
if (parsedComment == null) {
return '';
}
return [
for (var 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.
[ffi]: https://dart.dev/guides/libraries/c-interop
[meta-doNotStore]: https://pub.dev/documentation/meta/latest/meta/doNotStore-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-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
''');
var errorCodes = infoByName.keys.toList();
errorCodes.sort();
for (String errorCode in errorCodes) {
var info = infoByName[errorCode]!;
if (info.hasDocumentation) {
sink.writeln();
info.writeOn(sink);
}
}
}
/// Write the glossary.
void _writeGlossary(StringSink sink) {
sink.write(r'''
## Glossary
This page uses the following terms:
* [constant context][]
* [definite assignment][]
* [mixin application][]
* [override inference][]
* [part file][]
* [potentially non-nullable][]
* [public library][]
[constant context]: #constant-context
[definite assignment]: #definite-assignment
[mixin application]: #mixin-application
[override inference]: #override-inference
[part file]: #part-file
[potentially non-nullable]: #potentially-non-nullable
[public library]: #public-library
### Constant context
A _constant context_ is a region of code in which it isn't necessary to include
the `const` keyword because it's implied by the fact that everything in that
region is required to be a constant. The following locations are constant
contexts:
* Everything inside a list, map or set literal that's prefixed by the
`const` keyword. Example:
```dart
var l = const [/*constant context*/];
```
* The arguments inside an invocation of a constant constructor. Example:
```dart
var p = const Point(/*constant context*/);
```
* The initializer for a variable that's prefixed by the `const` keyword.
Example:
```dart
const v = /*constant context*/;
```
* Annotations
* The expression in a `case` clause. Example:
```dart
void f(int e) {
switch (e) {
case /*constant context*/:
break;
}
}
```
### Definite assignment
Definite assignment analysis is the process of determining, for each local
variable at each point in the code, which of the following is true:
- The variable has definitely been assigned a value (_definitely assigned_).
- The variable has definitely not been assigned a value (_definitely
unassigned_).
- The variable might or might not have been assigned a value, depending on the
execution path taken to arrive at that point.
Definite assignment analysis helps find problems in code, such as places where a
variable that might not have been assigned a value is being referenced, or
places where a variable that can only be assigned a value one time is being
assigned after it might already have been assigned a value.
For example, in the following code the variable `s` is definitely unassigned
when it’s passed as an argument to `print`:
```dart
void f() {
String s;
print(s);
}
```
But in the following code, the variable `s` is definitely assigned:
```dart
void f(String name) {
String s = 'Hello $name!';
print(s);
}
```
Definite assignment analysis can even tell whether a variable is definitely
assigned (or unassigned) when there are multiple possible execution paths. In
the following code the `print` function is called if execution goes through
either the true or the false branch of the `if` statement, but because `s` is
assigned no matter which branch is taken, it’s definitely assigned before it’s
passed to `print`:
```dart
void f(String name, bool casual) {
String s;
if (casual) {
s = 'Hi $name!';
} else {
s = 'Hello $name!';
}
print(s);
}
```
In flow analysis, the end of the `if` statement is referred to as a _join_—a
place where two or more execution paths merge back together. Where there's a
join, the analysis says that a variable is definitely assigned if it’s
definitely assigned along all of the paths that are merging, and definitely
unassigned if it’s definitely unassigned along all of the paths.
Sometimes a variable is assigned a value on one path but not on another, in
which case the variable might or might not have been assigned a value. In the
following example, the true branch of the `if` statement might or might not be
executed, so the variable might or might be assigned a value:
```dart
void f(String name, bool casual) {
String s;
if (casual) {
s = 'Hi $name!';
}
print(s);
}
```
The same is true if there is a false branch that doesn’t assign a value to `s`.
The analysis of loops is a little more complicated, but it follows the same
basic reasoning. For example, the condition in a `while` loop is always
executed, but the body might or might not be. So just like an `if` statement,
there's a join at the end of the `while` statement between the path in which the
condition is `true` and the path in which the condition is `false`.
For additional details, see the
[specification of definite assignment][definiteAssignmentSpec].
[definiteAssignmentSpec]: https://github.com/dart-lang/language/blob/master/resources/type-system/flow-analysis.md
### Mixin application
A _mixin application_ is the class created when a mixin is applied to a class.
For example, consider the following declarations:
```dart
class A {}
mixin M {}
class B extends A with M {}
```
The class `B` is a subclass of the mixin application of `M` to `A`, sometimes
nomenclated as `A+M`. The class `A+M` is a subclass of `A` and has members that
are copied from `M`.
You can give an actual name to a mixin application by defining it as:
```dart
class A {}
mixin M {}
class A_M = A with M;
```
Given this declaration of `A_M`, the following declaration of `B` is equivalent
to the declaration of `B` in the original example:
```dart
class B extends A_M {}
```
### Override inference
Override inference is the process by which any missing types in a method
declaration are inferred based on the corresponding types from the method or
methods that it overrides.
If a candidate method (the method that's missing type information) overrides a
single inherited method, then the corresponding types from the overridden method
are inferred. For example, consider the following code:
```dart
class A {
int m(String s) => 0;
}
class B extends A {
@override
m(s) => 1;
}
```
The declaration of `m` in `B` is a candidate because it's missing both the
return type and the parameter type. Because it overrides a single method (the
method `m` in `A`), the types from the overridden method will be used to infer
the missing types and it will be as if the method in `B` had been declared as
`int m(String s) => 1;`.
If a candidate method overrides multiple methods, and the function type one of
those overridden methods, M<sub>s</sub>, is a supertype of the function types of
all of the other overridden methods, then M<sub>s</sub> is used to infer the
missing types. For example, consider the following code:
```dart
class A {
int m(num n) => 0;
}
class B {
num m(int i) => 0;
}
class C implements A, B {
@override
m(n) => 1;
}
```
The declaration of `m` in `C` is a candidate for override inference because it's
missing both the return type and the parameter type. It overrides both `m` in
`A` and `m` in `B`, so we need to choose one of them from which the missing
types can be inferred. But because the function type of `m` in `A`
(`int Function(num)`) is a supertype of the function type of `m` in `B`
(`num Function(int)`), the function in `A` is used to infer the missing types.
The result is the same as declaring the method in `C` as `int m(num n) => 1;`.
It is an error if none of the overridden methods has a function type that is a
supertype of all the other overridden methods.
### Part file
A part file is a Dart source file that contains a `part of` directive.
### Potentially non-nullable
A type is _potentially non-nullable_ if it's either explicitly non-nullable or
if it's a type parameter.
A type is explicitly non-nullable if it is a type name that isn't followed by a
question mark. Note that there are a few types that are always nullable, such as
`Null` and `dynamic`, and that `FutureOr` is only non-nullable if it isn't
followed by a question mark _and_ the type argument is non-nullable (such as
`FutureOr<String>`).
Type parameters are potentially non-nullable because the actual runtime type
(the type specified as a type argument) might be non-nullable. For example,
given a declaration of `class C<T> {}`, the type `C` could be used with a
non-nullable type argument as in `C<int>`.
### Public library
A public library is a library that is located inside the package's `lib`
directory but not inside the `lib/src` directory.
''');
}
/// Write the header of the file.
void _writeHeader(StringSink sink) {
sink.write('''
---
title: Diagnostic messages
description: Details for diagnostics produced by the Dart analyzer.
---
{%- 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](/guides/language/analysis-options).
''');
}
}