Skip to content

Commit a0cc2dc

Browse files
authored
Add a nodoc configuration option to allow declaring entire files to not be documented (#2369)
* Add a nodoc dartdoc configuration option with tests * Add documentation for the nodoc configuration option. * Fix bug on empty doc comment * Sprinkle canonicalization fairy dust on paths for Windows * more fairy dust * pre-cleanup windows fix * Tighten up Windows support and add a test * Delete obsolete comment * Re-add dart:io for Platform
1 parent db8f78f commit a0cc2dc

13 files changed

+152
-20
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ dartdoc:
109109
categoryOrder: ["First Category", "Second Category"]
110110
examplePathPrefix: 'subdir/with/examples'
111111
includeExternal: ['bin/unusually_located_library.dart']
112+
nodoc: ['lib/sekret/*.dart']
112113
linkTo:
113114
url: "https://my.dartdocumentationsite.org/dev/%v%"
114115
showUndocumentedCategories: true
@@ -136,7 +137,7 @@ Unrecognized options will be ignored. Supported options:
136137
directives.
137138
* **exclude**: Specify a list of library names to avoid generating docs for,
138139
overriding any specified in include. All libraries listed must be local to this package, unlike
139-
the command line `--exclude`.
140+
the command line `--exclude`. See also `nodoc`.
140141
* **errors**: Specify warnings to be treated as errors. See the lists of valid warnings in the command
141142
line help for `--errors`, `--warnings`, and `--ignore`.
142143
* **favicon**: A path to a favicon for the generated docs.
@@ -175,6 +176,11 @@ Unrecognized options will be ignored. Supported options:
175176
* `%f%`: Relative path of file to the repository root
176177
* `%r%`: Revision
177178
* `%l%`: Line number
179+
* **nodoc**: Specify files (via globs) which should be treated as though they have the `@nodoc`
180+
tag in the documentation comment of every defined element. Unlike `exclude` this can specify
181+
source files directly, and neither inheritance nor reexports will cause these elements to be
182+
documented when included in other libraries. For more fine-grained control, use `@nodoc` in
183+
element documentation comments directly, or the `exclude` directive.
178184
* **warnings**: Specify otherwise ignored or set-to-error warnings to simply warn. See the lists
179185
of valid warnings in the command line help for `--errors`, `--warnings`, and `--ignore`.
180186

lib/src/dartdoc_options.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,8 @@ class DartdocOptionContext extends DartdocOptionContextBase
14891489
// ignore: unused_element
14901490
String get _linkToHosted => optionSet['linkTo']['hosted'].valueAt(context);
14911491

1492+
List<String> get nodoc => optionSet['nodoc'].valueAt(context);
1493+
14921494
String get output => optionSet['output'].valueAt(context);
14931495

14941496
PackageMeta get packageMeta => optionSet['packageMeta'].valueAt(context);
@@ -1669,6 +1671,11 @@ Future<List<DartdocOption<Object>>> createDartdocOptions(
16691671
help: 'Allow links to be generated for packages outside this one.',
16701672
negatable: true),
16711673
]),
1674+
DartdocOptionFileOnly<List<String>>('nodoc', [], resourceProvider,
1675+
optionIs: OptionKind.glob,
1676+
help: 'Dart symbols declared in these '
1677+
'files will be treated as though they have the @nodoc flag added to '
1678+
'their documentation comment.'),
16721679
DartdocOptionArgOnly<String>('output',
16731680
resourceProvider.pathContext.join('doc', 'api'), resourceProvider,
16741681
optionIs: OptionKind.dir, help: 'Path to output directory.'),

lib/src/model/documentation_comment.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ mixin DocumentationComment
4747
String computeDocumentationComment();
4848

4949
/// Returns true if the raw documentation comment has a nodoc indication.
50-
bool get hasNodoc =>
51-
documentationComment != null &&
52-
(documentationComment.contains('@nodoc') ||
53-
documentationComment.contains('<nodoc>'));
50+
bool get hasNodoc {
51+
if (documentationComment != null &&
52+
(documentationComment.contains('@nodoc') ||
53+
documentationComment.contains('<nodoc>'))) {
54+
return true;
55+
}
56+
return packageGraph.configSetsNodocFor(element.source.fullName);
57+
}
5458

5559
/// Process a [documentationComment], performing various actions based on
5660
/// `{@}`-style directives, except `{@tool}`, returning the processed result.

lib/src/model/model_element.dart

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -472,11 +472,7 @@ abstract class ModelElement extends Canonicalization
472472
!(enclosingElement as Extension).isPublic) {
473473
_isPublic = false;
474474
} else {
475-
if (documentationComment == null) {
476-
_isPublic = utils.hasPublicName(element);
477-
} else {
478-
_isPublic = utils.hasPublicName(element) && !hasNodoc;
479-
}
475+
_isPublic = utils.hasPublicName(element) && !hasNodoc;
480476
}
481477
}
482478
return _isPublic;

lib/src/model/package_graph.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:dartdoc/src/render/renderer_factory.dart';
2222
import 'package:dartdoc/src/special_elements.dart';
2323
import 'package:dartdoc/src/tuple.dart';
2424
import 'package:dartdoc/src/warnings.dart';
25+
import 'package:dartdoc/src/model_utils.dart' show matchGlobs;
2526

2627
class PackageGraph {
2728
PackageGraph.UninitializedPackageGraph(
@@ -950,6 +951,25 @@ class PackageGraph {
950951
allLocalModelElements.where((e) => e.isCanonical).toList();
951952
}
952953

954+
/// Glob lookups can be expensive. Cache per filename.
955+
final _configSetsNodocFor = HashMap<String, bool>();
956+
957+
/// Given an element's location, look up the nodoc configuration data and
958+
/// determine whether to unconditionally treat the element as "nodoc".
959+
bool configSetsNodocFor(String fullName) {
960+
if (!_configSetsNodocFor.containsKey(fullName)) {
961+
var file = resourceProvider.getFile(fullName);
962+
// Direct lookup instead of generating a custom context will save some
963+
// cycles. We can't use the element's [DartdocOptionContext] because that
964+
// might not be where the element was defined, which is what's important
965+
// for nodoc's semantics. Looking up the defining element just to pull
966+
// a context is again, slow.
967+
List<String> globs = config.optionSet['nodoc'].valueAt(file.parent);
968+
_configSetsNodocFor[fullName] = matchGlobs(globs, fullName);
969+
}
970+
return _configSetsNodocFor[fullName];
971+
}
972+
953973
String getMacro(String name) {
954974
assert(_localDocumentationBuilt);
955975
return _macros[name];

lib/src/model_utils.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,56 @@
55
library dartdoc.model_utils;
66

77
import 'dart:convert';
8+
import 'dart:io' show Platform;
89

910
import 'package:analyzer/dart/ast/ast.dart';
1011
import 'package:analyzer/dart/element/element.dart';
1112
import 'package:analyzer/file_system/file_system.dart';
1213
import 'package:analyzer/src/dart/ast/utilities.dart';
14+
import 'package:dartdoc/dartdoc.dart';
1315
import 'package:dartdoc/src/model/model.dart';
16+
import 'package:path/path.dart' as path;
17+
import 'package:glob/glob.dart';
18+
19+
final _driveLetterMatcher = RegExp(r'^\w:\\');
1420

1521
final Map<String, String> _fileContents = <String, String>{};
1622

23+
/// This will handle matching globs, including on Windows.
24+
///
25+
/// On windows, globs are assumed to use absolute Windows paths with drive
26+
/// letters in combination with globs, e.g. `C:\foo\bar\*.txt`. `fullName`
27+
/// also is assumed to have a drive letter.
28+
bool matchGlobs(List<String> globs, String fullName, {bool isWindows}) {
29+
isWindows ??= Platform.isWindows;
30+
var filteredGlobs = <String>[];
31+
32+
if (isWindows) {
33+
// TODO(jcollins-g): port this special casing to the glob package.
34+
var fullNameDriveLetter = _driveLetterMatcher.stringMatch(fullName);
35+
if (fullNameDriveLetter == null) {
36+
throw DartdocFailure(
37+
'Unable to recognize drive letter on Windows in: $fullName');
38+
}
39+
// Build a matcher from the [fullName]'s drive letter to filter the globs.
40+
var driveGlob = RegExp(fullNameDriveLetter.replaceFirst(r'\', r'\\'),
41+
caseSensitive: false);
42+
fullName = fullName.replaceFirst(_driveLetterMatcher, r'\');
43+
for (var glob in globs) {
44+
// Globs don't match if they aren't for the same drive.
45+
if (!driveGlob.hasMatch(glob)) continue;
46+
// `C:\` => `\` for rejoining via posix.
47+
glob = glob.replaceFirst(_driveLetterMatcher, r'/');
48+
filteredGlobs.add(path.posix.joinAll(path.windows.split(glob)));
49+
}
50+
} else {
51+
filteredGlobs.addAll(globs);
52+
}
53+
54+
return filteredGlobs.any((g) =>
55+
Glob(g, context: isWindows ? path.windows : null).matches(fullName));
56+
}
57+
1758
/// Returns the [AstNode] for a given [Element].
1859
///
1960
/// Uses a precomputed map of [element.source.fullName] to [CompilationUnit]

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies:
1212
collection: ^1.2.0
1313
cli_util: '>=0.1.4 <0.3.0'
1414
crypto: ^2.0.6
15+
glob: '>=1.1.2 <2.0.0'
1516
html: '>=0.12.1 <0.15.0'
1617
logging: ^0.11.3+1
1718
markdown: ^2.1.5
@@ -29,7 +30,6 @@ dev_dependencies:
2930
build_version: ^2.0.1
3031
coverage: ^0.14.0
3132
dhttpd: ^3.0.0
32-
glob: ^1.1.5
3333
grinder: ^0.8.2
3434
http: ^0.12.0
3535
pedantic: ^1.9.0

test/dartdoc_options_test.dart

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -383,13 +383,20 @@ dartdoc:
383383
});
384384

385385
group('glob options', () {
386+
String canonicalize(String path) =>
387+
resourceProvider.pathContext.canonicalize(path);
388+
386389
test('work via the command line', () {
387-
dartdocOptionSetAll.parseArguments(['--glob-option', 'foo/**']);
390+
dartdocOptionSetAll
391+
.parseArguments(['--glob-option', path.join('foo', '**')]);
388392
expect(
389393
dartdocOptionSetAll['globOption'].valueAtCurrent(),
390394
equals([
391-
resourceProvider.pathContext
392-
.join(resourceProvider.pathContext.current, 'foo/**')
395+
resourceProvider.pathContext.joinAll([
396+
canonicalize(resourceProvider.pathContext.current),
397+
'foo',
398+
'**'
399+
])
393400
]));
394401
});
395402

@@ -398,22 +405,26 @@ dartdoc:
398405
expect(
399406
dartdocOptionSetAll['globOption'].valueAt(secondDir),
400407
equals([
401-
resourceProvider.pathContext.join(secondDir.path, 'q*.html'),
402-
resourceProvider.pathContext.join(secondDir.path, 'e*.dart')
408+
canonicalize(
409+
resourceProvider.pathContext.join(secondDir.path, 'q*.html')),
410+
canonicalize(
411+
resourceProvider.pathContext.join(secondDir.path, 'e*.dart')),
403412
]));
404413
// No child override, should be the same as parent
405414
expect(
406415
dartdocOptionSetAll['globOption'].valueAt(secondDirSecondSub),
407416
equals([
408-
resourceProvider.pathContext.join(secondDir.path, 'q*.html'),
409-
resourceProvider.pathContext.join(secondDir.path, 'e*.dart')
417+
canonicalize(
418+
resourceProvider.pathContext.join(secondDir.path, 'q*.html')),
419+
canonicalize(
420+
resourceProvider.pathContext.join(secondDir.path, 'e*.dart')),
410421
]));
411422
// Child directory overrides
412423
expect(
413424
dartdocOptionSetAll['globOption'].valueAt(secondDirFirstSub),
414425
equals([
415-
resourceProvider.pathContext
416-
.join(secondDirFirstSub.path, '**/*.dart')
426+
resourceProvider.pathContext.joinAll(
427+
[canonicalize(secondDirFirstSub.path), '**', '*.dart'])
417428
]));
418429
});
419430
});

test/end2end/model_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,22 @@ void main() {
849849
});
850850
});
851851

852+
group('Comment processing', () {
853+
test('can virtually add nodoc via options file', () {
854+
var NodocMeLibrary = packageGraph.defaultPackage.allLibraries
855+
.firstWhere((l) => l.name == 'nodocme');
856+
expect(NodocMeLibrary.hasNodoc, isTrue);
857+
var NodocMeImplementation = fakeLibrary.allClasses
858+
.firstWhere((c) => c.name == 'NodocMeImplementation');
859+
expect(NodocMeImplementation.hasNodoc, isTrue);
860+
expect(NodocMeImplementation.isPublic, isFalse);
861+
var MeNeitherEvenWithoutADocComment = fakeLibrary.allClasses
862+
.firstWhere((c) => c.name == 'MeNeitherEvenWithoutADocComment');
863+
expect(MeNeitherEvenWithoutADocComment.hasNodoc, isTrue);
864+
expect(MeNeitherEvenWithoutADocComment.isPublic, isFalse);
865+
});
866+
});
867+
852868
group('doc references', () {
853869
String docsAsHtml;
854870

test/model_utils_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@ import 'package:dartdoc/src/model_utils.dart';
88
import 'package:test/test.dart';
99

1010
void main() {
11+
group('match glob', () {
12+
test('basic POSIX', () {
13+
expect(
14+
matchGlobs(['/a/b/*', '/b/c/*'], '/b/c/d', isWindows: false), isTrue);
15+
expect(matchGlobs(['/q/r/s'], '/foo', isWindows: false), isFalse);
16+
});
17+
18+
test('basic Windows', () {
19+
expect(matchGlobs([r'C:\a\b\*'], r'c:\a\b\d', isWindows: true), isTrue);
20+
});
21+
22+
test('Windows does not pass for different drive letters', () {
23+
expect(matchGlobs([r'C:\a\b\*'], r'D:\a\b\d', isWindows: true), isFalse);
24+
});
25+
});
26+
1127
group('model_utils stripIndentFromSource', () {
1228
test('no indent', () {
1329
expect(stripIndentFromSource('void foo() {\n print(1);\n}\n'),

testing/test_package/dartdoc_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ dartdoc:
77
Unreal:
88
markdown: "Unreal.md"
99
Real Libraries:
10+
nodoc: ["lib/src/nodoc*.dart"]
1011
tools:
1112
drill:
1213
command: ["bin/drill.dart"]

testing/test_package/lib/fake.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ import 'mylibpub.dart' as renamedLib2;
6060
import 'two_exports.dart' show BaseClass;
6161
export 'src/notadotdartfile';
6262

63+
// Verify that even though reexported, objects don't show in documentation.
64+
export 'package:test_package/src/nodocme.dart';
65+
6366
// ignore: uri_does_not_exist
6467
export 'package:test_package_imported/categoryExporting.dart'
6568
show IAmAClassWithCategories;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
///
2+
/// The library nodocme should never have any members documented, even if
3+
/// reexported, due to dartdoc_options.yaml in the package root.
4+
///
5+
library nodocme;
6+
7+
/// I should not appear in documentation.
8+
class NodocMeImplementation {}
9+
10+
class MeNeitherEvenWithoutADocComment {}
11+

0 commit comments

Comments
 (0)