Skip to content
Draft
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
131 changes: 131 additions & 0 deletions site/lib/src/models/lint_rules.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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' show jsonDecode;
import 'dart:io' show File;

import 'package:path/path.dart' as p;

import '../util.dart' show siteSrcDirectoryPath;

/// Reads and parses information about all non-internal lint rules from
/// the `src/data/lint-info.json` file.
List<LintRule> readAndLoadLints() {
if (_loadedLints case final alreadyLoadedLints?) return alreadyLoadedLints;

final lintRulesFile = File(
p.join(siteSrcDirectoryPath, 'data', 'lint-info.json'),
);
final rawLintRules =
jsonDecode(lintRulesFile.readAsStringSync()) as List<Object?>;

final lintRules = rawLintRules
.cast<Map<String, Object?>>()
.map(LintRule.fromJson)
.where((rule) => rule.latestState.type != LintStateType.internal)
.toList(growable: false);

return _loadedLints = lintRules;
}

/// A cache of the loaded and parsed lint rule info.
List<LintRule>? _loadedLints;

/// Represents a Dart analyzer lint rule with all its metadata.
final class LintRule {
/// The name of the lint rule.
final String name;

/// A short description of the lint rule.
final String description;

/// The categories of the lint rule such as 'style' and 'errorProne'.
final List<String> categories;

/// The states of the lint rule with version information.
final List<LintState> states;

/// The names of the lint rules incompatible with this one.
final List<String> incompatible;

/// The lint sets this rule is in (`core`, `recommended`, `flutter`).
final List<String> sets;

/// The status of the fix for this lint rule.
final LintFixStatus fixStatus;

/// The justification for the lint rule from deprecatedDetails.
final String justification;

/// Whether the lint rule has published diagnostic documentation.
final bool hasDiagnosticDocs;

const LintRule({
required this.name,
required this.description,
required this.categories,
required this.states,
required this.incompatible,
required this.sets,
required this.fixStatus,
required this.justification,
required this.hasDiagnosticDocs,
});

/// The most recent state of this lint.
LintState get latestState => states.last;

factory LintRule.fromJson(Map<String, Object?> json) => LintRule(
name: json['name'] as String,
description: json['description'] as String,
categories: (json['categories'] as List<Object?>).cast<String>(),
states: (json['states'] as List<Object?>)
.cast<Map<String, Object?>>()
.map(LintState.fromJson)
.toList(growable: false),
incompatible: (json['incompatible'] as List<Object?>).cast<String>(),
sets: (json['sets'] as List<Object?>).cast<String>(),
fixStatus: LintFixStatus.values.byName(json['fixStatus'] as String),
justification: json['justification'] as String,
hasDiagnosticDocs: json['hasDiagnosticDocs'] as bool,
);
}

/// Represents the state of a lint rule at a specific version.
final class LintState {
/// The type of state, such as stable or experimental.
final LintStateType type;

/// The SDK version when this state was introduced.
final String since;

const LintState({required this.type, required this.since});

/// If this lint is in a released of the SDK.
bool get isReleased {
final standardizedSince = since.trim().toLowerCase();
return standardizedSince != 'unreleased' &&
!standardizedSince.contains('-wip');
}

factory LintState.fromJson(Map<String, Object?> json) => LintState(
type: LintStateType.values.byName(json['type'] as String),
since: json['since'] as String,
);
}

enum LintFixStatus {
hasFix,
needsFix,
noFix,
unknown,
}

enum LintStateType {
deprecated,
experimental,
internal,
removed,
stable,
}
51 changes: 0 additions & 51 deletions site/lib/src/models/lints.dart

This file was deleted.

99 changes: 49 additions & 50 deletions site/lib/src/pages/custom_pages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:path/path.dart' as p;

import '../components/common/tags.dart';
import '../markdown/markdown_parser.dart';
import '../models/lints.dart';
import '../models/lint_rules.dart';
import 'glossary.dart';

/// All pages that should be loaded from memory rather than
Expand Down Expand Up @@ -56,14 +56,14 @@ List<MemoryPage> get _lintMemoryPages {
path: p.join(
'tools',
'linter-rules',
'${lint.id}.md',
'${lint.name}.md',
),
initialData: {
'page': <String, Object?>{
'title': lint.name,
'underscore_breaker_titles': true,
'description': 'Learn about the ${lint.name} linter rule.',
if (lint.state == 'removed') ...const {
if (lint.latestState.type == LintStateType.removed) ...const {
'noindex': true,
'sitemap': false,
} else
Expand All @@ -74,13 +74,14 @@ List<MemoryPage> get _lintMemoryPages {
},
builder: (context) {
final incompatibleLintsText = StringBuffer();
if (lint.incompatibleLints.isNotEmpty) {
if (lint.incompatible.isNotEmpty) {
incompatibleLintsText.writeln('## Incompatible rules\n');
incompatibleLintsText.writeln(
'The `${lint.id}` lint is incompatible with the following rules:',
'The `${lint.name}` lint is incompatible with '
'the following rules:',
);
incompatibleLintsText.writeln();
for (final incompatibleLint in lint.incompatibleLints) {
for (final incompatibleLint in lint.incompatible) {
incompatibleLintsText.writeln(
'- [`$incompatibleLint`](/tools/linter-rules/$incompatibleLint)',
);
Expand All @@ -90,63 +91,64 @@ List<MemoryPage> get _lintMemoryPages {
return Component.fragment(
[
Tags([
if (lint.sinceDartSdk == 'Unreleased' ||
lint.sinceDartSdk.contains('-wip'))
const Tag(
'Unreleased',
icon: 'pending',
switch (lint.latestState.type) {
LintStateType.deprecated => const Tag(
'Deprecated',
icon: 'report',
color: 'orange',
title: 'Lint is unreleased or work in progress.',
)
else if (lint.state == 'experimental')
const Tag(
title: 'Lint is deprecated.',
),
LintStateType.experimental => const Tag(
'Experimental',
icon: 'science',
color: 'orange',
title: 'Lint is experimental.',
)
else if (lint.state == 'deprecated')
const Tag(
'Deprecated',
icon: 'report',
color: 'orange',
title: 'Lint is deprecated.',
)
else if (lint.state == 'removed')
const Tag(
),
LintStateType.removed => const Tag(
'Removed',
icon: 'error',
color: 'red',
title: 'Lint has been removed.',
)
else
const Tag(
'Stable',
icon: 'verified_user',
color: 'green',
title: 'Lint is stable.',
),
LintStateType.stable =>
lint.latestState.isReleased
? const Tag(
'Stable',
icon: 'verified_user',
color: 'green',
title: 'Lint is stable.',
)
: const Tag(
'Unreleased',
icon: 'pending',
color: 'orange',
title: 'Lint is unreleased or work in progress.',
),
LintStateType.internal => throw StateError(
'An internal lint shouldn\'t be documented: ${lint.name}',
),
},

if (lint.lintSets.contains('core'))
if (lint.sets.contains('core'))
const Tag(
'Core',
icon: 'circles',
title: 'Lint is included in the core set of rules.',
)
else if (lint.lintSets.contains('recommended'))
else if (lint.sets.contains('recommended'))
const Tag(
'Recommended',
icon: 'thumb_up',
title: 'Lint is included in the recommended set of rules.',
)
else if (lint.lintSets.contains('flutter'))
else if (lint.sets.contains('flutter'))
const Tag(
'Flutter',
icon: 'flutter',
title: 'Lint is included in the Flutter set of rules.',
),

if (lint.fixStatus == 'hasFix')
if (lint.fixStatus == LintFixStatus.hasFix)
const Tag(
'Fix available',
icon: 'build',
Expand All @@ -160,7 +162,7 @@ ${lint.description}

## Details

${lint.docs}
${lint.justification}

$incompatibleLintsText
''',
Expand All @@ -171,22 +173,22 @@ $incompatibleLintsText
<a id="usage" aria-hidden="true"></a>
## Enable

To enable the `${lint.id}` rule, add `${lint.id}` under
To enable the `${lint.name}` rule, add `${lint.name}` under
**linter > rules** in your [`analysis_options.yaml`](/tools/analysis) file:

```yaml title="analysis_options.yaml"
linter:
rules:
- ${lint.id}
- ${lint.name}
```

If you're instead using the YAML map syntax to configure linter rules,
add `${lint.id}: true` under **linter > rules**:
add `${lint.name}: true` under **linter > rules**:

```yaml title="analysis_options.yaml"
linter:
rules:
${lint.id}: true
${lint.name}: true
```
''',
),
Expand All @@ -198,13 +200,10 @@ linter:
}

final linterRulesToShow = readAndLoadLints()
.where(
(lint) =>
lint.sinceDartSdk != 'Unreleased' &&
!lint.sinceDartSdk.contains('wip') &&
lint.state != 'removed' &&
lint.state != 'internal',
)
.where((lint) {
final latestState = lint.latestState;
return latestState.isReleased && latestState.type == LintStateType.stable;
})
.sortedBy((lint) => lint.name)
.toList(growable: false);

Expand All @@ -213,7 +212,7 @@ final linterRulesToShow = readAndLoadLints()
/// that are available in the current stable release.
MemoryPage get _allLinterRulesPage {
final allLinterRulesListAsString = linterRulesToShow
.map((lint) => ' - ${lint.id}')
.map((lint) => ' - ${lint.name}')
.join('\n');

return MemoryPage(
Expand Down Expand Up @@ -264,7 +263,7 @@ MemoryPage get _allLinterRulesJson => MemoryPage(
).convert([
for (final lint in linterRulesToShow)
{
'id': lint.id,
'id': lint.name,
},
]),
);
Loading