Skip to content

Commit 91c9ed9

Browse files
astashovkeertip
authored andcommitted
Add warnings for "free-hanging" generics in docs [#1250] (#1313)
* Add warnings for "free-hanging" generics in docs [#1250] @Hixie asked for warnings when the generics are "free-hanging", and not wrapped into [] block (like `Apple<Cat>`, but not `[Apple<Cat>]`). This commit adds those warnings. We try to find the tags, which are not wrapped into square brackets, which are not from the whitelist of possible HTML tags. Also, now output all the warnings to the console: * When there's a missing reference in the comments * When there're ambiguous reference in the comments * When there're "free-hanging" generics But that generates TONS AND TONS of warnings when you e.g. generate Dart SDK docs or Flutter docs. So, I've added the flag `--show-warnings`, without it we don't show the warnings. * Address feedback
1 parent 57c283e commit 91c9ed9

7 files changed

+130
-11
lines changed

bin/dartdoc.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ main(List<String> arguments) async {
140140
initializeConfig(
141141
addCrossdart: addCrossdart,
142142
examplePathPrefix: args['example-path-prefix'],
143+
showWarnings: args['show-warnings'],
143144
includeSource: includeSource,
144145
inputDir: inputDir,
145146
sdkVersion: sdk.sdkVersion);
@@ -176,6 +177,8 @@ ArgParser _createArgsParser() {
176177
defaultsTo: false);
177178
parser.addFlag('sdk-docs',
178179
help: 'Generate ONLY the docs for the Dart SDK.', negatable: false);
180+
parser.addFlag('show-warnings',
181+
help: 'Display warnings.', negatable: false);
179182
parser.addFlag('show-progress',
180183
help: 'Display progress indications to console stdout', negatable: false);
181184
parser.addOption('sdk-readme',

lib/dartdoc.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,14 @@ Future<List<Generator>> initGenerators(String url, List<String> headerFilePaths,
6464
void initializeConfig(
6565
{Directory inputDir,
6666
String sdkVersion,
67+
bool showWarnings: false,
6768
bool addCrossdart: false,
6869
String examplePathPrefix,
6970
bool includeSource: true}) {
7071
setConfig(
7172
inputDir: inputDir,
7273
sdkVersion: sdkVersion,
74+
showWarnings: showWarnings,
7375
addCrossdart: addCrossdart,
7476
examplePathPrefix: examplePathPrefix,
7577
includeSource: includeSource);

lib/src/config.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,23 @@ import 'dart:io';
99
class Config {
1010
final Directory inputDir;
1111
final bool addCrossdart;
12+
final bool showWarnings;
1213
final String examplePathPrefix;
1314
final bool includeSource;
1415
final String sdkVersion;
1516
Config._(
16-
this.inputDir, this.addCrossdart, this.examplePathPrefix, this.includeSource, this.sdkVersion);
17+
this.inputDir, this.showWarnings, this.addCrossdart, this.examplePathPrefix, this.includeSource, this.sdkVersion);
1718
}
1819

1920
Config _config;
2021
Config get config => _config;
2122

2223
void setConfig(
2324
{Directory inputDir,
25+
bool showWarnings: false,
2426
String sdkVersion,
2527
bool addCrossdart: false,
2628
String examplePathPrefix,
2729
bool includeSource: true}) {
28-
_config = new Config._(inputDir, addCrossdart, examplePathPrefix, includeSource, sdkVersion);
30+
_config = new Config._(inputDir, showWarnings, addCrossdart, examplePathPrefix, includeSource, sdkVersion);
2931
}

lib/src/markdown_processor.dart

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
library dartdoc.markdown_processor;
77

88
import 'dart:convert';
9+
import 'dart:math';
910

1011
import 'package:analyzer/dart/ast/ast.dart';
1112
import 'package:analyzer/dart/element/element.dart'
@@ -14,8 +15,23 @@ import 'package:html/parser.dart' show parse;
1415
import 'package:markdown/markdown.dart' as md;
1516

1617
import 'model.dart';
17-
18-
const bool _emitWarning = false;
18+
import 'reporting.dart';
19+
20+
const validHtmlTags = const [
21+
"a", "abbr", "address", "area", "article", "aside", "audio", "b",
22+
"bdi", "bdo", "blockquote", "br", "button", "canvas", "caption",
23+
"cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "dfn",
24+
"div", "dl", "dt", "em", "fieldset", "figcaption", "figure",
25+
"footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr",
26+
"i", "iframe", "img", "input", "ins", "kbd", "keygen", "label",
27+
"legend", "li", "link", "main", "map", "mark", "meta", "meter", "nav",
28+
"noscript", "object", "ol", "optgroup", "option", "output", "p", "param",
29+
"pre", "progress", "q", "s", "samp",
30+
"script", "section", "select", "small", "source", "span", "strong", "style",
31+
"sub", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th",
32+
"thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr"
33+
];
34+
final nonHTMLRegexp = new RegExp("</?(?!(${validHtmlTags.join("|")})[> ])\\w+[> ]");
1935

2036
// We don't emit warnings currently: #572.
2137
const List<String> _oneLinerSkipTags = const ["code", "pre"];
@@ -137,9 +153,8 @@ MatchingLinkResult _findRefElementInLibrary(String codeRef, ModelElement element
137153
} else if (result.length == 1) {
138154
return new MatchingLinkResult(result.values.first, result.values.first.name);
139155
} else {
140-
// TODO: add --fatal-warning, which would make the app crash in case of ambiguous references
141-
print(
142-
"Ambiguous reference to [${codeRef}] in '${element.fullyQualifiedName}' (${element.sourceFileName}:${element.lineNumber}). " +
156+
warning(
157+
"Ambiguous reference to [${codeRef}] in ${_elementLocation(element)}. " +
143158
"We found matches to the following elements: ${result.keys.map((k) => "'${k}'").join(", ")}");
144159
return new MatchingLinkResult(null, null);
145160
}
@@ -165,23 +180,75 @@ String _linkDocReference(String reference, ModelElement element, NodeList<Commen
165180
// different for doc references. sigh.
166181
return '<a ${classContent}href="${linkedElement.href}">$label</a>';
167182
} else {
168-
if (_emitWarning) {
169-
// TODO: add --fatal-warning, which would make the app crash in case of ambiguous references
170-
print(" warning: unresolved doc reference '$reference' (in $element)");
171-
}
183+
warning("unresolved doc reference '$reference' (in ${_elementLocation(element)}");
172184
return '<code>${HTML_ESCAPE.convert(label)}</code>';
173185
}
174186
}
175187

188+
String _elementLocation(ModelElement element) {
189+
while ((element.element.documentationComment == null || element.element.documentationComment == "")
190+
&& element.overriddenElement != null) {
191+
element = element.overriddenElement;
192+
}
193+
return "'${element.fullyQualifiedName}' (${element.sourceFileName}:${element.lineNumber})";
194+
}
195+
176196
String _renderMarkdownToHtml(String text, [ModelElement element]) {
177197
md.Node _linkResolver(String name) {
178198
NodeList<CommentReference> commentRefs = _getCommentRefs(element);
179199
return new md.Text(_linkDocReference(name, element, commentRefs));
180200
}
181201

202+
_showWarningsForGenericsOutsideSquareBracketsBlocks(text, element);
182203
return md.markdownToHtml(text, inlineSyntaxes: _markdown_syntaxes, linkResolver: _linkResolver);
183204
}
184205

206+
// Generics should be wrapped into `[]` blocks, to avoid handling them as HTML tags
207+
// (like, [Apple<int>]). @Hixie asked for a warning when there's something, that looks
208+
// like a non HTML tag (a generic?) outside of a `[]` block.
209+
// https://github.com/dart-lang/dartdoc/issues/1250#issuecomment-269257942
210+
void _showWarningsForGenericsOutsideSquareBracketsBlocks(String text, [ModelElement element]) {
211+
List<int> tagPositions = findFreeHangingGenericsPositions(text);
212+
if (tagPositions.isNotEmpty) {
213+
tagPositions.forEach((int position) {
214+
String errorMessage = "Generic type handled as HTML";
215+
if (element != null) {
216+
errorMessage += " in ${_elementLocation(element)}";
217+
}
218+
errorMessage += " - '${text.substring(max(position - 20, 0), min(position + 20, text.length))}'";
219+
warning(errorMessage);
220+
});
221+
}
222+
}
223+
224+
List<int> findFreeHangingGenericsPositions(String string) {
225+
int currentPosition = 0;
226+
int squareBracketsDepth = 0;
227+
List<int> results = [];
228+
while (true) {
229+
final int nextOpenBracket = string.indexOf("[", currentPosition);
230+
final int nextCloseBracket = string.indexOf("]", currentPosition);
231+
final int nextNonHTMLTag = string.indexOf(nonHTMLRegexp, currentPosition);
232+
final Iterable<int> nextPositions = [nextOpenBracket, nextCloseBracket, nextNonHTMLTag].where((p) => p != -1);
233+
if (nextPositions.isNotEmpty) {
234+
final minPos = nextPositions.reduce(min);
235+
if (nextOpenBracket == minPos) {
236+
squareBracketsDepth += 1;
237+
} else if (nextCloseBracket == minPos) {
238+
squareBracketsDepth = max(squareBracketsDepth - 1, 0);
239+
} else if (nextNonHTMLTag == minPos) {
240+
if (squareBracketsDepth == 0) {
241+
results.add(minPos);
242+
}
243+
}
244+
currentPosition = minPos + 1;
245+
} else {
246+
break;
247+
}
248+
}
249+
return results;
250+
}
251+
185252
class Documentation {
186253
final String raw;
187254
final String asHtml;

lib/src/reporting.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library reporting;
6+
7+
import 'config.dart';
8+
import 'dart:io';
9+
10+
void warning(String message) {
11+
// TODO: Could handle fatal warnings here, or print to stderr, or remember
12+
// that we had at least one warning, and exit with non-null exit code in this case.
13+
if (config != null && config.showWarnings) {
14+
stderr.writeln("warning: ${message}");
15+
}
16+
}

test/all.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'model_utils_test.dart' as model_utils_tests;
1313
import 'package_meta_test.dart' as package_meta_tests;
1414
import 'resource_loader_test.dart' as resource_loader_tests;
1515
import 'template_test.dart' as template_tests;
16+
import 'markdown_processor_test.dart' as markdown_processor_tests;
1617

1718
void main() {
1819
compare_output_tests.main();
@@ -24,4 +25,5 @@ void main() {
2425
package_meta_tests.main();
2526
resource_loader_tests.main();
2627
template_tests.main();
28+
markdown_processor_tests.main();
2729
}

test/markdown_processor_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
library dartdoc.markdown_processor_test;
6+
7+
import 'package:dartdoc/src/markdown_processor.dart';
8+
import 'package:test/test.dart';
9+
10+
void main() {
11+
group('findFreeHangingGenericsPositions()', () {
12+
test('returns empty array if all the generics are in []', () {
13+
final string = "One two [three<four>] [[five<six>] seven eight";
14+
expect(findFreeHangingGenericsPositions(string), equals([]));
15+
});
16+
17+
test('returns positions of generics outside of []', () {
18+
final string = "One two<int> [[three<four>] five<six>] seven<eight>";
19+
expect(findFreeHangingGenericsPositions(string), equals([7, 44]));
20+
});
21+
22+
test('ignores HTML tags', () {
23+
final string = "One two<int> foo<pre> [[three<four>] five<six>] bar</pre> seven<eight>";
24+
expect(findFreeHangingGenericsPositions(string), equals([7, 63]));
25+
});
26+
});
27+
}

0 commit comments

Comments
 (0)