Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

[wip] Introduce DartDoc "Summary Line" lint #3820

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 26 additions & 22 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# 1.34.0

- new lint: `dartdoc_summary_line`

# 1.33.0

- fix `unnecessary_parenthesis` false-positive with null-aware expressions
- fix `void_checks` to allow assignments of `Future<dynamic>?` to parameters
- fix `void_checks` to allow assignments of `Future<dynamic>?` to parameters
typed `FutureOr<void>?`
- removed support for:
- `enable_null_safety`
Expand All @@ -13,7 +17,7 @@
- update `void_checks` to allow returning `Never` as void
- new lint: `unnecessary_breaks`
- fix `use_build_context_synchronously` in if conditions
- update `no_adjacent_strings_in_list` to support set literals and for- and
- update `no_adjacent_strings_in_list` to support set literals and for- and
if-elements

# 1.32.0
Expand Down Expand Up @@ -44,14 +48,14 @@
- new lint: `dangling_library_doc_comments`
- fix `no_leading_underscores_for_local_identifiers` to not report super formals as local variables
- fix `unnecessary_overrides` false negatives
- fix `cancel_subscriptions` for nullable fields
- fix `cancel_subscriptions` for nullable fields
- new lint: `collection_methods_unrelated_type`
- update `library_names` to support unnamed libraries
- fix `unnecessary_parenthesis` support for as-expressions
- fix `use_build_context_synchronously` to check for context property accesses
- fix false positive in `comment_references`
- improved unrelated type checks to handle enums and cascades
- fix `unnecessary_brace_in_string_interps` for `this` expressions
- fix `unnecessary_brace_in_string_interps` for `this` expressions
- update `use_build_context_synchronously` for `BuildContext.mounted`
- improve `flutter_style_todos` to handle more cases
- fix `use_build_context_synchronously` to check for `BuildContext`s in named expressions
Expand Down Expand Up @@ -89,7 +93,7 @@

# 1.27.0

- fix `avoid_redundant_argument_values` when referencing required
- fix `avoid_redundant_argument_values` when referencing required
parameters in legacy libraries
- performance improvements for `use_late_for_private_fields_and_variables`
- new lint: `use_string_in_part_of_directives`
Expand Down Expand Up @@ -142,7 +146,7 @@
# 1.23.0

- fixed `no_leading_underscores_for_local_identifiers`
to lint local function declarations
to lint local function declarations
- fixed `avoid_init_to_null` to correctly handle super
initializing defaults that are non-null
- updated `no_leading_underscores_for_local_identifiers`
Expand All @@ -151,7 +155,7 @@
start with a digit
- updated `require_trailing_commas` to handle functions
in asserts and multi-line strings
- updated `unsafe_html` to allow assignments to
- updated `unsafe_html` to allow assignments to
`img.src`
- fixed `unnecessary_null_checks` to properly handle map
literal entries
Expand Down Expand Up @@ -180,7 +184,7 @@
with `key` super parameter initializers
- fixed `use_super_parameters` false positive with field
formal params
- updated `unnecessary_null_checks` and
- updated `unnecessary_null_checks` and
`null_check_on_nullable_type_parameter` to handle
list/set/map literals, and `yield` and `await`
expressions
Expand All @@ -198,7 +202,7 @@
- new lint: `use_colored_box`
- performance improvements for `sort_constructors`
- doc improvements for `always_use_package_imports`,
`avoid_print`, and `avoid_relative_lib_imports`
`avoid_print`, and `avoid_relative_lib_imports`
- update `avoid_void_async` to skip `main` functions
- update `prefer_final_parameters` to not super on super params
- lint updates for enhanced-enums and super-initializer language
Expand All @@ -209,7 +213,7 @@
# 1.18.0

- extend `camel_case_types` to cover enums
- fix `no_leading_underscores_for_local_identifiers` to not
- fix `no_leading_underscores_for_local_identifiers` to not
mis-flag field formal parameters with default values
- fix `prefer_function_declarations_over_variables` to not
mis-flag non-final fields
Expand All @@ -233,7 +237,7 @@
- updates to `secure_pubspec_urls` to check `issue_tracker` and
`repository` entries
- new lint: `conditional_uri_does_not_exist`
- performance improvements for
- performance improvements for
`missing_whitespace_between_adjacent_strings`

# 1.15.0
Expand All @@ -248,13 +252,13 @@

# 1.14.0

- fix `omit_local_variable_types` to not flag a local type that is
- fix `omit_local_variable_types` to not flag a local type that is
required for inference

# 1.13.0

- allow `while (true) { ...}` in `literal_only_boolean_expressions`
- fixed `file_names` to report at the start of the file (not the entire
- fixed `file_names` to report at the start of the file (not the entire
compilation unit)
- fixed `prefer_collection_literals` named typed param false positive
- control flow improvements for `use_build_context_synchronously`
Expand All @@ -268,19 +272,19 @@

# 1.11.0

- added support for constructor tear-offs to `avoid_redundant_argument_values`,
- added support for constructor tear-offs to `avoid_redundant_argument_values`,
`unnecessary_lambdas`, and `unnecessary_parenthesis`
- new lint: `unnecessary_constructor_name` to flag unnecessary uses of `.new`

# 1.10.0

- improved regular expression parsing performance for common checks
- improved regular expression parsing performance for common checks
(`camel_case_types`, `file_names`, etc.)
- (internal) migrated to analyzer 2.1.0 APIs
- fixed false positive in `use_build_context_synchronously` in awaits inside
- fixed false positive in `use_build_context_synchronously` in awaits inside
anonymous functions
- fixed `overridden_fields` false positive w/ static fields
- fixed false positive in `avoid_null_checks_in_equality_operators` w/
- fixed false positive in `avoid_null_checks_in_equality_operators` w/
non-nullable params
- fixed false positive for deferred imports in `prefer_const_constructors`

Expand Down Expand Up @@ -312,7 +316,7 @@
- fixed `curly_braces_in_flow_control_structures` to properly flag terminating `else-if`
blocks
- improved `always_specify_types` to support type aliases
- fixed false positive in `unnecessary_string_interpolations` w/ nullable interpolated
- fixed false positive in `unnecessary_string_interpolations` w/ nullable interpolated
strings
- fixed false positive in `avoid_function_literals_in_foreach_calls` for nullable
iterables
Expand All @@ -330,7 +334,7 @@
# 1.7.0

- fixed case-sensitive false positive in `use_full_hex_values_for_flutter_colors`
- improved try-block and switch statement flow analysis for
- improved try-block and switch statement flow analysis for
`use_build_context_synchronously`
- updated `use_setters_to_change_properties` to only highlight a method name,
not the entire body and doc comment
Expand All @@ -343,7 +347,7 @@

# 1.6.1

- reverted relaxation of `sort_child_properties_last` to allow for a
- reverted relaxation of `sort_child_properties_last` to allow for a
trailing Widget in instance creations

# 1.6.0
Expand All @@ -352,14 +356,14 @@
underscore
- fixed false negative in `prefer_final_parameters` where first parameter
is final
- improved `directives_ordering` sorting of directives with dot paths and
- improved `directives_ordering` sorting of directives with dot paths and
dot-separated package names
- relaxed `sort_child_properties_last` to allow for a trailing Widget in
instance creations

# 1.5.0

- (internal) migrated to `SecurityLintCode` instead of deprecated
- (internal) migrated to `SecurityLintCode` instead of deprecated
`SecurityLintCodeWithUniqueName`
- (internal) fixed `avoid_types_as_parameter_names` to skip field formal
parameters
Expand Down
2 changes: 2 additions & 0 deletions lib/src/rules.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import 'rules/constant_identifier_names.dart';
import 'rules/control_flow_in_finally.dart';
import 'rules/curly_braces_in_flow_control_structures.dart';
import 'rules/dangling_library_doc_comments.dart';
import 'rules/dartdoc_summary_line.dart';
import 'rules/depend_on_referenced_packages.dart';
import 'rules/deprecated_consistency.dart';
import 'rules/diagnostic_describe_all_properties.dart';
Expand Down Expand Up @@ -227,6 +228,7 @@ import 'rules/void_checks.dart';
void registerLintRules({bool inTestMode = false}) {
Analyzer.facade.cacheLinterVersion();
Analyzer.facade
..register(DartdocSummaryLine())
..register(AlwaysDeclareReturnTypes())
..register(UnnecessaryLibraryDirective())
..register(AlwaysPutControlBodyOnNewLine())
Expand Down
152 changes: 152 additions & 0 deletions lib/src/rules/dartdoc_summary_line.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';

import '../analyzer.dart';

const _desc =
r'Start your DartDoc comment with a single, brief sentence that ends with a period.';

const _details = r'''
From [Effective Dart](https://dart.dev/guides/language/effective-dart/documentation#do-start-doc-comments-with-a-single-sentence-summary):

**DO** start doc comments with a single-sentence summary.

Start your doc comment with a brief, user-centric description ending with a
period. A sentence fragment is often sufficient. Provide just enough context
for the reader to orient themselves and decide if they should keep reading or
look elsewhere for the solution to their problem.

For example:

**GOOD:**
```dart
/// Deletes the file at [path] from the file system.
void delete(String path) {
...
}
```

**BAD:**
```dart
/// Depending on the state of the file system and the user's permissions,
/// certain operations may or may not be possible. If there is no file at
/// [path] or it can't be accessed, this function throws either [IOError]
/// or [PermissionError], respectively. Otherwise, this deletes the file.
void delete(String path) {
...
}
```

From [Effective Dart](https://dart.dev/guides/language/effective-dart/documentation#do-separate-the-first-sentence-of-a-doc-comment-into-its-own-paragraph):

**DO** separate the first sentence of a doc comment into its own paragraph.

Add a blank line after the first sentence to split it out into its own
paragraph. If more than a single sentence of explanation is useful, put the
rest in later paragraphs.

This helps you write a tight first sentence that summarizes the documentation.
Also, tools like dart doc use the first paragraph as a short summary in places
like lists of classes and members.

For example:

**GOOD:**
```dart
/// Deletes the file at [path].
///
/// Throws an [IOError] if the file could not be found. Throws a
/// [PermissionError] if the file is present but could not be deleted.
void delete(String path) {
...
}
```

**BAD:**
```dart
/// Deletes the file at [path]. Throws an [IOError] if the file could not
/// be found. Throws a [PermissionError] if the file is present but could
/// not be deleted.
void delete(String path) {
...
}
```
''';

/// Enforces that DartDoc blocks start with a single line that ends in a period.
///
/// (Like the one above!)
class DartdocSummaryLine extends LintRule {
DartdocSummaryLine()
: super(
name: 'dartdoc_summary_line',
description: _desc,
details: _details,
group: Group.style);

@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this);
registry.addComment(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
final LintRule rule;

_Visitor(this.rule);

/// The summary line should be 'brief' according to the Dart recommendations,
/// however 'brief' is not defined perfectly. Instead of enforcing a fixed
/// number of characters, we just look at how many lines are part of the
/// summary, and if we deem them to be too many (2 should be plenty), we fail
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need some data on this point. If plenty of "fine" doc comments exist which are written across more than 2 lines, we'll have to adjust, so that the rule can be used.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since part of this is geared towards the generated Dartdoc... Do we have examples where the generated dartdoc looks good/bad because of the summary, and try to infer what's a good limit here? IIRC Dartdoc trimmed this at some point? (or I remember a [...] which pointed to the full doc page or something?)

If unlimited, people can write walls of text as the "first line" of the dartdoc, and that kind of defeats the purpose of the lint I think :/

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by: Given that it's important for the lint and for dartdoc to agree, have we considered moving the logic that dartdoc uses to find the summary into the markdown package (or maybe even to the analyzer package as a getter on Comment) so that it can be shared? Having the logic in two places opens the door for them becoming inconsistent in the future.

Copy link
Contributor

@srawlins srawlins Nov 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dartdoc takes the inner HTML of the first HTML node of the rendered documentation. We could move this logic to be shared, but it requires dartdoc's custom resolution logic, so we'd have to move that too.

https://github.com/dart-lang/dartdoc/blob/master/lib/src/render/documentation_renderer.dart#L61

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how does it decide what to include in the first HTML node?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It parses the documentation comment and renders it into HTML. So the user determines what is in the first HTML node.

In this markdown:

header
===

text

the first HTML node is <h1>header</h1>, whose inner HTML is header. If the first HTML node is a <p> node with a gigabyte of HTML inside, then that is the first node, and the gigabyte of HTML is displayed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. I should probably drop out of the conversation with apologies for distracting everyone. It just seemed to me that the logic dartdoc / markdown uses to determine what's in the first HTML node could be used without needing to first build the HTML and then pull it back out. But if it isn't worth the effort then it isn't.

/// the lint
///
/// The above summary fails with all the checks that this lint adds, it has
/// more than one sentence, is too long, and doesn't add a period at the end.
///
/// It has it all!
@override
void visitComment(Comment node) {
if (!node.isDocumentation || !node.tokens.first.lexeme.startsWith('///')) {
// Ignore /** comments.
return;
}

Iterable<Token> summary = _getSummary(node);
if (summary.length > 1) {
int length = summary.last.charEnd - summary.first.charOffset;
rule.reportLintForOffset(summary.first.charOffset, length);
return;
}

Token token = summary.last; // and only
if (!_endsWithPeriod(token)) {
rule.reportLintForToken(token);
return;
}
}
}

// Some utility functions

/// Removes the first triple slash of a `line` and returns its trimmed value.
String _trimSlashes(String line) => line.replaceFirst('///', '').trim();

/// Determines if a token `tk` represents an empty comment line.
bool _isNotBlankLine(Token tk) => _trimSlashes(tk.lexeme).isNotEmpty;

/// Retrieves the summary line of a [Comment] `node`.
///
/// This will take tokens (each comment line) until an empty line is found.
Iterable<Token> _getSummary(Comment node) =>
node.tokens.takeWhile(_isNotBlankLine);

/// Checks that the last of the `lines` ends in a period.
bool _endsWithPeriod(Token token) => token.lexeme.endsWith('.');