diff --git a/README.md b/README.md index f201eacc47..c9f0bc6c88 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ dartdoc: categoryOrder: ["First Category", "Second Category"] examplePathPrefix: 'subdir/with/examples' includeExternal: ['bin/unusually_located_library.dart'] + nodoc: ['lib/sekret/*.dart'] linkTo: url: "https://my.dartdocumentationsite.org/dev/%v%" showUndocumentedCategories: true @@ -136,7 +137,7 @@ Unrecognized options will be ignored. Supported options: directives. * **exclude**: Specify a list of library names to avoid generating docs for, overriding any specified in include. All libraries listed must be local to this package, unlike - the command line `--exclude`. + the command line `--exclude`. See also `nodoc`. * **errors**: Specify warnings to be treated as errors. See the lists of valid warnings in the command line help for `--errors`, `--warnings`, and `--ignore`. * **favicon**: A path to a favicon for the generated docs. @@ -175,6 +176,11 @@ Unrecognized options will be ignored. Supported options: * `%f%`: Relative path of file to the repository root * `%r%`: Revision * `%l%`: Line number + * **nodoc**: Specify files (via globs) which should be treated as though they have the `@nodoc` + tag in the documentation comment of every defined element. Unlike `exclude` this can specify + source files directly, and neither inheritance nor reexports will cause these elements to be + documented when included in other libraries. For more fine-grained control, use `@nodoc` in + element documentation comments directly, or the `exclude` directive. * **warnings**: Specify otherwise ignored or set-to-error warnings to simply warn. See the lists of valid warnings in the command line help for `--errors`, `--warnings`, and `--ignore`. diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart index b60c8f0c74..be76f8968d 100644 --- a/lib/src/dartdoc_options.dart +++ b/lib/src/dartdoc_options.dart @@ -1489,6 +1489,8 @@ class DartdocOptionContext extends DartdocOptionContextBase // ignore: unused_element String get _linkToHosted => optionSet['linkTo']['hosted'].valueAt(context); + List get nodoc => optionSet['nodoc'].valueAt(context); + String get output => optionSet['output'].valueAt(context); PackageMeta get packageMeta => optionSet['packageMeta'].valueAt(context); @@ -1669,6 +1671,11 @@ Future>> createDartdocOptions( help: 'Allow links to be generated for packages outside this one.', negatable: true), ]), + DartdocOptionFileOnly>('nodoc', [], resourceProvider, + optionIs: OptionKind.glob, + help: 'Dart symbols declared in these ' + 'files will be treated as though they have the @nodoc flag added to ' + 'their documentation comment.'), DartdocOptionArgOnly('output', resourceProvider.pathContext.join('doc', 'api'), resourceProvider, optionIs: OptionKind.dir, help: 'Path to output directory.'), diff --git a/lib/src/model/documentation_comment.dart b/lib/src/model/documentation_comment.dart index 7db25befa3..81ddd2ead4 100644 --- a/lib/src/model/documentation_comment.dart +++ b/lib/src/model/documentation_comment.dart @@ -47,10 +47,14 @@ mixin DocumentationComment String computeDocumentationComment(); /// Returns true if the raw documentation comment has a nodoc indication. - bool get hasNodoc => - documentationComment != null && - (documentationComment.contains('@nodoc') || - documentationComment.contains('')); + bool get hasNodoc { + if (documentationComment != null && + (documentationComment.contains('@nodoc') || + documentationComment.contains(''))) { + return true; + } + return packageGraph.configSetsNodocFor(element.source.fullName); + } /// Process a [documentationComment], performing various actions based on /// `{@}`-style directives, except `{@tool}`, returning the processed result. diff --git a/lib/src/model/model_element.dart b/lib/src/model/model_element.dart index 0f25cfa53a..d3e0bf4758 100644 --- a/lib/src/model/model_element.dart +++ b/lib/src/model/model_element.dart @@ -472,11 +472,7 @@ abstract class ModelElement extends Canonicalization !(enclosingElement as Extension).isPublic) { _isPublic = false; } else { - if (documentationComment == null) { - _isPublic = utils.hasPublicName(element); - } else { - _isPublic = utils.hasPublicName(element) && !hasNodoc; - } + _isPublic = utils.hasPublicName(element) && !hasNodoc; } } return _isPublic; diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart index ba3d9a9e58..ba30ef678b 100644 --- a/lib/src/model/package_graph.dart +++ b/lib/src/model/package_graph.dart @@ -22,6 +22,7 @@ import 'package:dartdoc/src/render/renderer_factory.dart'; import 'package:dartdoc/src/special_elements.dart'; import 'package:dartdoc/src/tuple.dart'; import 'package:dartdoc/src/warnings.dart'; +import 'package:dartdoc/src/model_utils.dart' show matchGlobs; class PackageGraph { PackageGraph.UninitializedPackageGraph( @@ -950,6 +951,25 @@ class PackageGraph { allLocalModelElements.where((e) => e.isCanonical).toList(); } + /// Glob lookups can be expensive. Cache per filename. + final _configSetsNodocFor = HashMap(); + + /// Given an element's location, look up the nodoc configuration data and + /// determine whether to unconditionally treat the element as "nodoc". + bool configSetsNodocFor(String fullName) { + if (!_configSetsNodocFor.containsKey(fullName)) { + var file = resourceProvider.getFile(fullName); + // Direct lookup instead of generating a custom context will save some + // cycles. We can't use the element's [DartdocOptionContext] because that + // might not be where the element was defined, which is what's important + // for nodoc's semantics. Looking up the defining element just to pull + // a context is again, slow. + List globs = config.optionSet['nodoc'].valueAt(file.parent); + _configSetsNodocFor[fullName] = matchGlobs(globs, fullName); + } + return _configSetsNodocFor[fullName]; + } + String getMacro(String name) { assert(_localDocumentationBuilt); return _macros[name]; diff --git a/lib/src/model_utils.dart b/lib/src/model_utils.dart index f798b6c649..77a143247a 100644 --- a/lib/src/model_utils.dart +++ b/lib/src/model_utils.dart @@ -5,15 +5,56 @@ library dartdoc.model_utils; import 'dart:convert'; +import 'dart:io' show Platform; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/src/dart/ast/utilities.dart'; +import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/model/model.dart'; +import 'package:path/path.dart' as path; +import 'package:glob/glob.dart'; + +final _driveLetterMatcher = RegExp(r'^\w:\\'); final Map _fileContents = {}; +/// This will handle matching globs, including on Windows. +/// +/// On windows, globs are assumed to use absolute Windows paths with drive +/// letters in combination with globs, e.g. `C:\foo\bar\*.txt`. `fullName` +/// also is assumed to have a drive letter. +bool matchGlobs(List globs, String fullName, {bool isWindows}) { + isWindows ??= Platform.isWindows; + var filteredGlobs = []; + + if (isWindows) { + // TODO(jcollins-g): port this special casing to the glob package. + var fullNameDriveLetter = _driveLetterMatcher.stringMatch(fullName); + if (fullNameDriveLetter == null) { + throw DartdocFailure( + 'Unable to recognize drive letter on Windows in: $fullName'); + } + // Build a matcher from the [fullName]'s drive letter to filter the globs. + var driveGlob = RegExp(fullNameDriveLetter.replaceFirst(r'\', r'\\'), + caseSensitive: false); + fullName = fullName.replaceFirst(_driveLetterMatcher, r'\'); + for (var glob in globs) { + // Globs don't match if they aren't for the same drive. + if (!driveGlob.hasMatch(glob)) continue; + // `C:\` => `\` for rejoining via posix. + glob = glob.replaceFirst(_driveLetterMatcher, r'/'); + filteredGlobs.add(path.posix.joinAll(path.windows.split(glob))); + } + } else { + filteredGlobs.addAll(globs); + } + + return filteredGlobs.any((g) => + Glob(g, context: isWindows ? path.windows : null).matches(fullName)); +} + /// Returns the [AstNode] for a given [Element]. /// /// Uses a precomputed map of [element.source.fullName] to [CompilationUnit] diff --git a/pubspec.yaml b/pubspec.yaml index 6dd816039e..fb316fd6a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: collection: ^1.2.0 cli_util: '>=0.1.4 <0.3.0' crypto: ^2.0.6 + glob: '>=1.1.2 <2.0.0' html: '>=0.12.1 <0.15.0' logging: ^0.11.3+1 markdown: ^2.1.5 @@ -29,7 +30,6 @@ dev_dependencies: build_version: ^2.0.1 coverage: ^0.14.0 dhttpd: ^3.0.0 - glob: ^1.1.5 grinder: ^0.8.2 http: ^0.12.0 pedantic: ^1.9.0 diff --git a/test/dartdoc_options_test.dart b/test/dartdoc_options_test.dart index c454e4293f..0a7b1cb5d3 100644 --- a/test/dartdoc_options_test.dart +++ b/test/dartdoc_options_test.dart @@ -383,13 +383,20 @@ dartdoc: }); group('glob options', () { + String canonicalize(String path) => + resourceProvider.pathContext.canonicalize(path); + test('work via the command line', () { - dartdocOptionSetAll.parseArguments(['--glob-option', 'foo/**']); + dartdocOptionSetAll + .parseArguments(['--glob-option', path.join('foo', '**')]); expect( dartdocOptionSetAll['globOption'].valueAtCurrent(), equals([ - resourceProvider.pathContext - .join(resourceProvider.pathContext.current, 'foo/**') + resourceProvider.pathContext.joinAll([ + canonicalize(resourceProvider.pathContext.current), + 'foo', + '**' + ]) ])); }); @@ -398,22 +405,26 @@ dartdoc: expect( dartdocOptionSetAll['globOption'].valueAt(secondDir), equals([ - resourceProvider.pathContext.join(secondDir.path, 'q*.html'), - resourceProvider.pathContext.join(secondDir.path, 'e*.dart') + canonicalize( + resourceProvider.pathContext.join(secondDir.path, 'q*.html')), + canonicalize( + resourceProvider.pathContext.join(secondDir.path, 'e*.dart')), ])); // No child override, should be the same as parent expect( dartdocOptionSetAll['globOption'].valueAt(secondDirSecondSub), equals([ - resourceProvider.pathContext.join(secondDir.path, 'q*.html'), - resourceProvider.pathContext.join(secondDir.path, 'e*.dart') + canonicalize( + resourceProvider.pathContext.join(secondDir.path, 'q*.html')), + canonicalize( + resourceProvider.pathContext.join(secondDir.path, 'e*.dart')), ])); // Child directory overrides expect( dartdocOptionSetAll['globOption'].valueAt(secondDirFirstSub), equals([ - resourceProvider.pathContext - .join(secondDirFirstSub.path, '**/*.dart') + resourceProvider.pathContext.joinAll( + [canonicalize(secondDirFirstSub.path), '**', '*.dart']) ])); }); }); diff --git a/test/end2end/model_test.dart b/test/end2end/model_test.dart index d116a5af7c..70e1f6978a 100644 --- a/test/end2end/model_test.dart +++ b/test/end2end/model_test.dart @@ -849,6 +849,22 @@ void main() { }); }); + group('Comment processing', () { + test('can virtually add nodoc via options file', () { + var NodocMeLibrary = packageGraph.defaultPackage.allLibraries + .firstWhere((l) => l.name == 'nodocme'); + expect(NodocMeLibrary.hasNodoc, isTrue); + var NodocMeImplementation = fakeLibrary.allClasses + .firstWhere((c) => c.name == 'NodocMeImplementation'); + expect(NodocMeImplementation.hasNodoc, isTrue); + expect(NodocMeImplementation.isPublic, isFalse); + var MeNeitherEvenWithoutADocComment = fakeLibrary.allClasses + .firstWhere((c) => c.name == 'MeNeitherEvenWithoutADocComment'); + expect(MeNeitherEvenWithoutADocComment.hasNodoc, isTrue); + expect(MeNeitherEvenWithoutADocComment.isPublic, isFalse); + }); + }); + group('doc references', () { String docsAsHtml; diff --git a/test/model_utils_test.dart b/test/model_utils_test.dart index 7bc35cdfe2..850c2d4901 100644 --- a/test/model_utils_test.dart +++ b/test/model_utils_test.dart @@ -8,6 +8,22 @@ import 'package:dartdoc/src/model_utils.dart'; import 'package:test/test.dart'; void main() { + group('match glob', () { + test('basic POSIX', () { + expect( + matchGlobs(['/a/b/*', '/b/c/*'], '/b/c/d', isWindows: false), isTrue); + expect(matchGlobs(['/q/r/s'], '/foo', isWindows: false), isFalse); + }); + + test('basic Windows', () { + expect(matchGlobs([r'C:\a\b\*'], r'c:\a\b\d', isWindows: true), isTrue); + }); + + test('Windows does not pass for different drive letters', () { + expect(matchGlobs([r'C:\a\b\*'], r'D:\a\b\d', isWindows: true), isFalse); + }); + }); + group('model_utils stripIndentFromSource', () { test('no indent', () { expect(stripIndentFromSource('void foo() {\n print(1);\n}\n'), diff --git a/testing/test_package/dartdoc_options.yaml b/testing/test_package/dartdoc_options.yaml index 2e1d9d5321..97922cd395 100644 --- a/testing/test_package/dartdoc_options.yaml +++ b/testing/test_package/dartdoc_options.yaml @@ -7,6 +7,7 @@ dartdoc: Unreal: markdown: "Unreal.md" Real Libraries: + nodoc: ["lib/src/nodoc*.dart"] tools: drill: command: ["bin/drill.dart"] diff --git a/testing/test_package/lib/fake.dart b/testing/test_package/lib/fake.dart index 6493d369e5..004599bda9 100644 --- a/testing/test_package/lib/fake.dart +++ b/testing/test_package/lib/fake.dart @@ -60,6 +60,9 @@ import 'mylibpub.dart' as renamedLib2; import 'two_exports.dart' show BaseClass; export 'src/notadotdartfile'; +// Verify that even though reexported, objects don't show in documentation. +export 'package:test_package/src/nodocme.dart'; + // ignore: uri_does_not_exist export 'package:test_package_imported/categoryExporting.dart' show IAmAClassWithCategories; diff --git a/testing/test_package/lib/src/nodocme.dart b/testing/test_package/lib/src/nodocme.dart new file mode 100644 index 0000000000..457dcaa1af --- /dev/null +++ b/testing/test_package/lib/src/nodocme.dart @@ -0,0 +1,11 @@ +/// +/// The library nodocme should never have any members documented, even if +/// reexported, due to dartdoc_options.yaml in the package root. +/// +library nodocme; + +/// I should not appear in documentation. +class NodocMeImplementation {} + +class MeNeitherEvenWithoutADocComment {} +