blob: cb46a205d3c3b2577d64d92b907ca0ff5311bb22 [file] [log] [blame]
// Copyright (c) 2015, 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/src/lint/config.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:args/args.dart';
import 'package:http/http.dart' as http;
import 'package:linter/src/analyzer.dart';
import 'package:linter/src/rules.dart';
import 'package:markdown/markdown.dart';
import 'machine.dart';
import 'since.dart';
/// Generates lint rule docs for publishing to https://dart-lang.github.io/
void main([List<String> args]) async {
var parser = ArgParser()
..addOption('out', abbr: 'o', help: 'Specifies output directory.');
ArgResults options;
try {
options = parser.parse(args);
} on FormatException catch (err) {
printUsage(parser, err.message);
return;
}
var outDir = options['out'] as String;
await generateDocs(outDir);
}
const ruleFootMatter = '''
In addition, rules can be further distinguished by *maturity*. Unqualified
rules are considered stable, while others may be marked **experimental**
to indicate that they are under review. Lints that are marked as **deprecated**
should not be used and are subject to removal in future Linter releases.
Rules can be selectively enabled in the analyzer using
[analysis options](https://pub.dev/packages/analyzer)
or through an
[analysis options file](https://dart.dev/guides/language/analysis-options#the-analysis-options-file).
* **An auto-generated list enabling all options is provided [here](options/options.html).**
As some lints may contradict each other, only a subset of these will be
enabled in practice, but this list should provide a convenient jumping-off point.
Many lints are included in various predefined rulesets:
* [pedantic](https://github.com/dart-lang/pedantic) for rules enforced internally at Google
* [effective_dart](https://github.com/tenhobi/effective_dart) for rules corresponding to the [Effective Dart](https://dart.dev/guides/language/effective-dart) style guide
* [flutter](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/analysis_options_user.yaml) for rules used in <code>flutter analyze</code>
Rules included in these rulesets are badged in the documentation below.
These rules are under active development. Feedback is
[welcome](https://github.com/dart-lang/linter/issues)!
''';
const ruleLeadMatter = 'Rules are organized into familiar rule groups.';
final effectiveDartRules = <String>[];
final flutterRules = <String>[];
final pedanticRules = <String>[];
/// Sorted list of contributed lint rules.
final List<LintRule> rules =
List<LintRule>.from(Registry.ruleRegistry, growable: false)..sort();
Map<String, SinceInfo> sinceInfo;
Future<String> get effectiveDartLatestVersion async {
var url =
'https://raw.githubusercontent.com/tenhobi/effective_dart/master/lib/analysis_options.yaml';
var client = http.Client();
print('loading $url...');
var req = await client.get(url);
var parts = req.body.split('package:effective_dart/analysis_options.');
return parts[1].split('.yaml')[0];
}
String get enumerateErrorRules => rules
.where((r) => r.group == Group.errors)
.map((r) => '${toDescription(r)}')
.join('\n\n');
String get enumerateGroups => Group.builtin
.map((Group g) =>
'<li><strong>${g.name} -</strong> ${markdownToHtml(g.description)}</li>')
.join('\n');
String get enumeratePubRules => rules
.where((r) => r.group == Group.pub)
.map((r) => '${toDescription(r)}')
.join('\n\n');
String get enumerateStyleRules => rules
.where((r) => r.group == Group.style)
.map((r) => '${toDescription(r)}')
.join('\n\n');
Future<String> get pedanticLatestVersion async {
var url =
'https://raw.githubusercontent.com/dart-lang/pedantic/master/lib/analysis_options.yaml';
var client = http.Client();
print('loading $url...');
var req = await client.get(url);
var parts = req.body.split('package:pedantic/analysis_options.');
return parts[1].split('.yaml')[0];
}
String describeMaturity(LintRule r) =>
r.maturity == Maturity.stable ? '' : ' (${r.maturity.name})';
Future<void> fetchBadgeInfo() async {
var latestPedantic = await pedanticLatestVersion;
var latestEffectiveDart = await effectiveDartLatestVersion;
var pedantic = await fetchConfig(
'https://raw.githubusercontent.com/dart-lang/pedantic/master/lib/analysis_options.$latestPedantic.yaml');
for (var ruleConfig in pedantic.ruleConfigs) {
pedanticRules.add(ruleConfig.name);
}
var effectiveDart = await fetchConfig(
'https://raw.githubusercontent.com/tenhobi/effective_dart/master/lib/analysis_options.$latestEffectiveDart.yaml');
for (var ruleConfig in effectiveDart.ruleConfigs) {
effectiveDartRules.add(ruleConfig.name);
}
var flutter = await fetchConfig(
'https://raw.githubusercontent.com/flutter/flutter/master/packages/flutter/lib/analysis_options_user.yaml');
for (var ruleConfig in flutter.ruleConfigs) {
flutterRules.add(ruleConfig.name);
}
}
Future<LintConfig> fetchConfig(String url) async {
var client = http.Client();
print('loading $url...');
var req = await client.get(url);
return processAnalysisOptionsFile(req.body);
}
Future<void> fetchSinceInfo() async {
sinceInfo = await sinceMap;
}
Future<void> generateDocs(String dir) async {
var outDir = dir;
if (outDir != null) {
final d = Directory(outDir);
if (!d.existsSync()) {
print("Directory '${d.path}' does not exist");
return;
}
if (!File('$outDir/options').existsSync()) {
final lintsChildDir = Directory('$outDir/lints');
if (lintsChildDir.existsSync()) {
outDir = lintsChildDir.path;
}
}
}
registerLintRules();
// Generate lint count badge.
await CountBadger(Registry.ruleRegistry).generate(outDir);
// Fetch info for lint group/style badge generation.
await fetchBadgeInfo();
// Fetch since info.
await fetchSinceInfo();
// Generate rule files.
rules.forEach((l) {
RuleHtmlGenerator(l).generate(outDir);
RuleMarkdownGenerator(l).generate(filePath: outDir);
});
// Generate index.
HtmlIndexer(Registry.ruleRegistry).generate(outDir);
MarkdownIndexer(Registry.ruleRegistry).generate(filePath: outDir);
// Generate options samples.
OptionsSample(rules).generate(outDir);
// Generate a machine-readable summary of rules.
MachineSummaryGenerator(Registry.ruleRegistry).generate(outDir);
}
String getBadges(String rule) {
var sb = StringBuffer();
if (flutterRules.contains(rule)) {
sb.write(
'<a class="style-type" href="https://github.com/flutter/flutter/blob/master/packages/flutter/lib/analysis_options_user.yaml">'
'<!--suppress HtmlUnknownTarget --><img alt="flutter" src="style-flutter.svg"></a>');
}
if (pedanticRules.contains(rule)) {
sb.write(
'<a class="style-type" href="https://github.com/dart-lang/pedantic/#enabled-lints">'
'<!--suppress HtmlUnknownTarget --><img alt="pedantic" src="style-pedantic.svg"></a>');
}
if (effectiveDartRules.contains(rule)) {
sb.write(
'<a class="style-type" href="https://github.com/tenhobi/effective_dart">'
'<!--suppress HtmlUnknownTarget --><img alt="effective dart" src="style-effective_dart.svg"></a>');
}
return sb.toString();
}
void printUsage(ArgParser parser, [String error]) {
var message = 'Generates lint docs.';
if (error != null) {
message = error;
}
stdout.write('''$message
Usage: doc
${parser.usage}
''');
}
String qualify(LintRule r) => r.name.toString() + describeMaturity(r);
String toDescription(LintRule r) =>
'<!--suppress HtmlUnknownTarget --><strong><a href = "${r.name}.html">${qualify(r)}</a></strong><br/> ${getBadges(r.name)} ${markdownToHtml(r.description)}';
class CountBadger {
Iterable<LintRule> rules;
CountBadger(this.rules);
Future<void> generate(String dirPath) async {
var lintCount = rules.length;
var client = http.Client();
var req = await client.get(
Uri.parse('https://img.shields.io/badge/lints-$lintCount-blue.svg'));
var bytes = req.bodyBytes;
await File('$dirPath/count-badge.svg').writeAsBytes(bytes);
}
}
class HtmlIndexer {
final Iterable<LintRule> rules;
HtmlIndexer(this.rules);
void generate(String filePath) {
var generated = _generate();
if (filePath != null) {
var outPath = '$filePath/index.html';
print('Writing to $outPath');
File(outPath).writeAsStringSync(generated);
} else {
print(generated);
}
}
String _generate() => '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../dart-192.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="../styles.css">
<title>Linter for Dart</title>
</head>
<body>
<div class="wrapper">
<header>
<a href="../index.html">
<h1>Linter for Dart</h1>
</a>
<p>Lint Rules</p>
<ul>
<li><a href="https://dart.dev/guides/language/analysis-options#enabling-linter-rules">Using the <strong>Linter</strong></a></li>
</ul>
<p><a class="overflow-link" href="https://dart.dev/guides/language/analysis-options#enabling-linter-rules">Using the <strong>Linter</strong></a></p>
</header>
<section>
<h1>Supported Lint Rules</h1>
<p>
This list is auto-generated from our sources.
</p>
${markdownToHtml(ruleLeadMatter)}
<ul>
$enumerateGroups
</ul>
${markdownToHtml(ruleFootMatter)}
<h2>Error Rules</h2>
$enumerateErrorRules
<h2>Style Rules</h2>
$enumerateStyleRules
<h2>Pub Rules</h2>
$enumeratePubRules
</section>
</div>
<footer>
<p>Maintained by the <a href="https://dart.dev/">Dart Team</a></p>
<p>Visit us on <a href="https://github.com/dart-lang/linter">Github</a></p>
</footer>
</body>
</html>
''';
}
class MachineSummaryGenerator {
final Iterable<LintRule> rules;
MachineSummaryGenerator(this.rules);
void generate(String filePath) {
var generated = getMachineListing(rules);
if (filePath != null) {
var outPath = '$filePath/machine/rules.json';
print('Writing to $outPath');
File(outPath).writeAsStringSync(generated);
} else {
print(generated);
}
}
}
class MarkdownIndexer {
final Iterable<LintRule> rules;
MarkdownIndexer(this.rules);
void generate({String filePath}) {
final buffer = StringBuffer();
buffer.writeln('# Linter for Dart');
buffer.writeln();
buffer.writeln('## Lint Rules');
buffer.writeln();
buffer.writeln(
'[Using the Linter](https://dart.dev/guides/language/analysis-options#enabling-linter-rules)');
buffer.writeln();
buffer.writeln('## Supported Lint Rules');
buffer.writeln();
buffer.writeln('This list is auto-generated from our sources.');
buffer.writeln();
buffer.writeln(ruleLeadMatter);
buffer.writeln();
for (var group in Group.builtin) {
buffer.writeln('- **${group.name}** - ${group.description}');
buffer.writeln();
}
buffer.writeln(ruleFootMatter);
buffer.writeln();
var emit = (LintRule rule) {
buffer
.writeln('**[${rule.name}](${rule.name}.md)** - ${rule.description}');
if (flutterRules.contains(rule.name)) {
buffer.writeln('[![flutter](style-flutter.svg)]'
'(https://github.com/flutter/flutter/blob/master/packages/'
'flutter/lib/analysis_options_user.yaml)');
}
if (pedanticRules.contains(rule.name)) {
buffer.writeln('[![pedantic](style-pedantic.svg)]'
'(https://github.com/dart-lang/pedantic/#enabled-lints)');
}
if (effectiveDartRules.contains(rule.name)) {
buffer.writeln('[![effective dart](style-effective_dart.svg)]'
'(https://github.com/tenhobi/effective_dart)');
}
buffer.writeln();
};
buffer.writeln('## Error Rules');
buffer.writeln();
// ignore: prefer_foreach
for (var rule in rules.where((rule) => rule.group == Group.errors)) {
emit(rule);
}
buffer.writeln('## Style Rules');
buffer.writeln();
// ignore: prefer_foreach
for (var rule in rules.where((rule) => rule.group == Group.style)) {
emit(rule);
}
buffer.writeln('## Pub Rules');
buffer.writeln();
// ignore: prefer_foreach
for (var rule in rules.where((rule) => rule.group == Group.pub)) {
emit(rule);
}
if (filePath == null) {
print(buffer.toString());
} else {
File('$filePath/index.md').writeAsStringSync(buffer.toString());
}
}
}
class OptionsSample {
Iterable<LintRule> rules;
OptionsSample(this.rules);
void generate(String filePath) {
var generated = _generate();
if (filePath != null) {
var outPath = '$filePath/options/options.html';
print('Writing to $outPath');
File(outPath).writeAsStringSync(generated);
} else {
print(generated);
}
}
String generateOptions() {
final sb = StringBuffer('''
```
linter:
rules:
''');
var sortedRules = rules
.where((r) => r.maturity != Maturity.deprecated)
.map((r) => r.name)
.toList()
..sort();
for (var rule in sortedRules) {
sb.write(' - $rule\n');
}
sb.write('```');
return sb.toString();
}
String _generate() => '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../../dart-192.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="stylesheet" href="../../styles.css">
<title>Analysis Options</title>
</head>
<body>
<div class="wrapper">
<header>
<a href="../../index.html">
<h1>Linter for Dart</h1>
</a>
<p>Analysis Options</p>
<ul>
<li><a href="../index.html">View all <strong>Lint Rules</strong></a></li>
<li><a href="https://dart.dev/guides/language/analysis-options#enabling-linter-rules">Using the <strong>Linter</strong></a></li>
</ul>
<p><a class="overflow-link" href="../index.html">View all <strong>Lint Rules</strong></a></p>
<p><a class="overflow-link" href="https://dart.dev/guides/language/analysis-options#enabling-linter-rules">Using the <strong>Linter</strong></a></p>
</header>
<section>
<h1 id="analysis-options">Analysis Options</h1>
<p>
Auto-generated options enabling all lints.
Add these to your
<a href="https://dart.dev/guides/language/analysis-options#the-analysis-options-file">analysis_options.yaml file</a>
and tailor to fit!
</p>
${markdownToHtml(generateOptions())}
</section>
</div>
<footer>
<p>Maintained by the <a href="https://dart.dev/">Dart Team</a></p>
<p>Visit us on <a href="https://github.com/dart-lang/linter">Github</a></p>
</footer>
</body>
</html>
''';
}
class RuleHtmlGenerator {
final LintRule rule;
RuleHtmlGenerator(this.rule);
String get details => rule.details ?? '';
String get group => rule.group.name;
String get humanReadableName => rule.name;
String get incompatibleRuleDetails {
final sb = StringBuffer();
var incompatibleRules = rule.incompatibleRules;
if (incompatibleRules.isNotEmpty) {
sb.writeln('<p>');
sb.write('Incompatible with: ');
var rule = incompatibleRules.first;
sb.write(
'<!--suppress HtmlUnknownTarget --><a href = "$rule.html" >$rule</a>');
for (var i = 1; i < incompatibleRules.length; ++i) {
rule = incompatibleRules[i];
sb.write(', <a href = "$rule.html" >$rule</a>');
}
sb.writeln('.');
sb.writeln('</p>');
}
return sb.toString();
}
String get maturity => rule.maturity.name;
String get maturityString {
switch (rule.maturity) {
case Maturity.deprecated:
return '<span style="color:orangered;font-weight:bold;" >$maturity</span>';
case Maturity.experimental:
return '<span style="color:hotpink;font-weight:bold;" >$maturity</span>';
default:
return maturity;
}
}
String get name => rule.name;
String get since {
var info = sinceInfo[name];
var version = info.sinceDartSdk != null
? '>= ${info.sinceDartSdk}'
: '<strong>unreleased</strong>';
return 'Dart SDK: $version • <small>(Linter v${info.sinceLinter})</small>';
}
void generate([String filePath]) {
var generated = _generate();
if (filePath != null) {
var outPath = '$filePath/$name.html';
print('Writing to $outPath');
File(outPath).writeAsStringSync(generated);
} else {
print(generated);
}
}
String _generate() => '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="../dart-192.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>$name</title>
<link rel="stylesheet" href="../styles.css">
</head>
<body>
<div class="wrapper">
<header>
<h1>$humanReadableName</h1>
<p>Group: $group</p>
<p>Maturity: $maturityString</p>
<div class="tooltip">
<p>$since</p>
<span class="tooltip-content">Since info is static, may be stale</span>
</div>
${getBadges(name)}
<ul>
<li><a href="index.html">View all <strong>Lint Rules</strong></a></li>
<li><a href="https://dart.dev/guides/language/analysis-options#enabling-linter-rules">Using the <strong>Linter</strong></a></li>
</ul>
<p><a class="overflow-link" href="index.html">View all <strong>Lint Rules</strong></a></p>
<p><a class="overflow-link" href="https://dart.dev/guides/language/analysis-options#enabling-linter-rules">Using the <strong>Linter</strong></a></p>
</header>
<section>
${markdownToHtml(details)}
$incompatibleRuleDetails
</section>
</div>
<footer>
<p>Maintained by the <a href="https://dart.dev/">Dart Team</a></p>
<p>Visit us on <a href="https://github.com/dart-lang/linter">Github</a></p>
</footer>
</body>
</html>
''';
}
class RuleMarkdownGenerator {
final LintRule rule;
RuleMarkdownGenerator(this.rule);
String get details => rule.details ?? '';
String get group => rule.group.name;
String get maturity => rule.maturity.name;
String get name => rule.name;
String get since {
var info = sinceInfo[name];
var version = info.sinceDartSdk != null
? '>= ${info.sinceDartSdk}'
: '**unreleased**';
return 'Dart SDK: $version • (Linter v${info.sinceLinter})';
}
void generate({String filePath}) {
final buffer = StringBuffer();
buffer.writeln('# Rule $name');
buffer.writeln();
buffer.writeln('**Group**: $group\\');
buffer.writeln('**Maturity**: $maturity\\');
buffer.writeln('**Since**: $since\\');
buffer.writeln();
// badges
if (flutterRules.contains(name)) {
buffer.writeln('[![flutter](style-flutter.svg)]'
'(https://github.com/flutter/flutter/blob/master/packages/'
'flutter/lib/analysis_options_user.yaml)');
}
if (pedanticRules.contains(name)) {
buffer.writeln('[![pedantic](style-pedantic.svg)]'
'(https://github.com/dart-lang/pedantic/#enabled-lints)');
}
if (effectiveDartRules.contains(name)) {
buffer.writeln('[![effective dart](style-effective_dart.svg)]'
'(https://github.com/tenhobi/effective_dart)');
}
buffer.writeln();
buffer.writeln('## Description');
buffer.writeln();
buffer.writeln('${details.trim()}');
// incompatible rules
final incompatibleRules = rule.incompatibleRules;
if (incompatibleRules.isNotEmpty) {
buffer.writeln('## Incompatible With');
buffer.writeln();
for (var rule in incompatibleRules) {
buffer.writeln('- [$rule]($rule.md)');
}
buffer.writeln();
}
if (filePath == null) {
print(buffer.toString());
} else {
File('$filePath/$name.md').writeAsStringSync(buffer.toString());
}
}
}