| // Copyright (c) 2017, 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/dart/element/element.dart'; |
| import 'package:dartdoc/src/dartdoc_options.dart'; |
| import 'package:dartdoc/src/logging.dart'; |
| import 'package:dartdoc/src/model.dart'; |
| import 'package:dartdoc/src/package_meta.dart'; |
| import 'package:dartdoc/src/tuple.dart'; |
| |
| abstract class PackageWarningOptionContext implements DartdocOptionContextBase { |
| bool get allowNonLocalWarnings => |
| optionSet['allowNonLocalWarnings'].valueAt(context); |
| // allowWarningsInPackages, ignoreWarningsInPackages, errors, warnings, and ignore |
| // are only used indirectly via the synthetic packageWarningOptions option. |
| PackageWarningOptions get packageWarningOptions => |
| optionSet['packageWarningOptions'].valueAt(context); |
| bool get verboseWarnings => optionSet['verboseWarnings'].valueAt(context); |
| } |
| |
| Future<List<DartdocOption>> createPackageWarningOptions() async { |
| return <DartdocOption>[ |
| new DartdocOptionArgOnly<bool>('allowNonLocalWarnings', false, |
| negatable: true, |
| help: 'Show warnings from packages we are not documenting locally.'), |
| |
| // Options for globally enabling/disabling all warnings and errors |
| // for individual packages are command-line only. This will allow |
| // meta-packages like Flutter to control whether warnings are displayed for |
| // packages they don't control. |
| new DartdocOptionArgOnly<List<String>>('allowWarningsInPackages', null, |
| help: |
| 'Package names to display warnings for (ignore all others if set).'), |
| new DartdocOptionArgOnly<List<String>>('allowErrorsInPackages', null, |
| help: 'Package names to display errors for (ignore all others if set)'), |
| new DartdocOptionArgOnly<List<String>>('ignoreWarningsInPackages', null, |
| help: |
| 'Package names to ignore warnings for. Takes priority over allow-warnings-in-packages'), |
| new DartdocOptionArgOnly<List<String>>('ignoreErrorsInPackages', null, |
| help: |
| 'Package names to ignore errors for. Takes priority over allow-errors-in-packages'), |
| // Options for globally enabling/disabling warnings and errors across |
| // packages. Loaded from dartdoc_options.yaml, but command line arguments |
| // will override. |
| new DartdocOptionArgFile<List<String>>('errors', null, |
| help: |
| 'Additional warning names to force as errors. Specify an empty list to force defaults (overriding dartdoc_options.yaml)\nDefaults:\n' + |
| (packageWarningDefinitions.values |
| .where((d) => |
| d.defaultWarningMode == PackageWarningMode.error) |
| .toList() |
| ..sort()) |
| .map((d) => ' ${d.warningName}: ${d.shortHelp}') |
| .join('\n')), |
| new DartdocOptionArgFile<List<String>>('ignore', null, |
| help: |
| 'Additional warning names to ignore. Specify an empty list to force defaults (overriding dartdoc_options.yaml).\nDefaults:\n' + |
| (packageWarningDefinitions.values |
| .where((d) => |
| d.defaultWarningMode == PackageWarningMode.ignore) |
| .toList() |
| ..sort()) |
| .map((d) => ' ${d.warningName}: ${d.shortHelp}') |
| .join('\n')), |
| new DartdocOptionArgFile<List<String>>('warnings', null, |
| help: |
| 'Additional warning names to show as warnings (instead of error or ignore, if not warning by default).\nDefaults:\n' + |
| (packageWarningDefinitions.values |
| .where((d) => |
| d.defaultWarningMode == PackageWarningMode.warn) |
| .toList() |
| ..sort()) |
| .map((d) => ' ${d.warningName}: ${d.shortHelp}') |
| .join('\n')), |
| // Synthetic option uses a factory to build a PackageWarningOptions from all the above flags. |
| new DartdocOptionSyntheticOnly<PackageWarningOptions>( |
| 'packageWarningOptions', PackageWarningOptions.fromOptions), |
| ]; |
| } |
| |
| class PackageWarningDefinition implements Comparable<PackageWarningDefinition> { |
| final String warningName; |
| final String shortHelp; |
| final List<String> longHelp; |
| final PackageWarning kind; |
| final PackageWarningMode defaultWarningMode; |
| |
| const PackageWarningDefinition(this.kind, this.warningName, this.shortHelp, |
| {List<String> longHelp, PackageWarningMode defaultWarningMode}) |
| : this.longHelp = longHelp ?? const [], |
| this.defaultWarningMode = defaultWarningMode ?? PackageWarningMode.warn; |
| |
| @override |
| int compareTo(PackageWarningDefinition other) { |
| return warningName.compareTo(other.warningName); |
| } |
| } |
| |
| /// Same as [packageWarningDefinitions], except keyed by the warning name. |
| final Map<String, PackageWarningDefinition> packageWarningsByName = |
| new Map.fromEntries(packageWarningDefinitions.values |
| .map((definition) => new MapEntry(definition.warningName, definition))); |
| |
| /// Provides description text and command line flags for warnings. |
| /// TODO(jcollins-g): Actually use this for command line flags. |
| final Map<PackageWarning, PackageWarningDefinition> packageWarningDefinitions = |
| const { |
| PackageWarning.ambiguousDocReference: const PackageWarningDefinition( |
| PackageWarning.ambiguousDocReference, |
| "ambiguous-doc-reference", |
| "A comment reference could refer to two or more different objects"), |
| PackageWarning.ambiguousReexport: const PackageWarningDefinition( |
| PackageWarning.ambiguousReexport, |
| "ambiguous-reexport", |
| "A symbol is exported from private to public in more than one library and dartdoc can not determine which one is canonical", |
| longHelp: const [ |
| "Use {@canonicalFor @@name@@} in the desired library's documentation to resolve", |
| "the ambiguity and/or override dartdoc's decision, or structure your package ", |
| "so the reexport is less ambiguous. The symbol will still be referenced in ", |
| "all candidates -- this only controls the location where it will be written ", |
| "and which library will be displayed in navigation for the relevant pages.", |
| "The flag --ambiguous-reexport-scorer-min-confidence allows you to set the", |
| "threshold at which this warning will appear." |
| ]), |
| PackageWarning.ignoredCanonicalFor: const PackageWarningDefinition( |
| PackageWarning.ignoredCanonicalFor, |
| "ignored-canonical-for", |
| "A @canonicalFor tag refers to a library which this symbol can not be canonical for"), |
| PackageWarning.noCanonicalFound: const PackageWarningDefinition( |
| PackageWarning.noCanonicalFound, |
| "no-canonical-found", |
| "A symbol is part of the public interface for this package, but no library documented with this package documents it so dartdoc can not link to it"), |
| PackageWarning.noLibraryLevelDocs: const PackageWarningDefinition( |
| PackageWarning.noLibraryLevelDocs, |
| "no-library-level-docs", |
| "There are no library level docs for this library"), |
| PackageWarning.packageOrderGivesMissingPackageName: |
| const PackageWarningDefinition( |
| PackageWarning.packageOrderGivesMissingPackageName, |
| "category-order-gives-missing-package-name", |
| "The category-order flag on the command line was given the name of a nonexistent package"), |
| PackageWarning.reexportedPrivateApiAcrossPackages: const PackageWarningDefinition( |
| PackageWarning.reexportedPrivateApiAcrossPackages, |
| "reexported-private-api-across-packages", |
| "One or more libraries reexports private API members from outside its own package"), |
| PackageWarning.unresolvedDocReference: const PackageWarningDefinition( |
| PackageWarning.unresolvedDocReference, |
| "unresolved-doc-reference", |
| "A comment reference could not be found in parameters, enclosing class, enclosing library, or at the top level of any documented library with the package"), |
| PackageWarning.brokenLink: const PackageWarningDefinition( |
| PackageWarning.brokenLink, |
| "broken-link", |
| "Dartdoc generated a link to a non-existent file"), |
| PackageWarning.unknownMacro: const PackageWarningDefinition( |
| PackageWarning.unknownMacro, |
| "unknown-macro", |
| "A comment reference contains an unknown macro"), |
| PackageWarning.orphanedFile: const PackageWarningDefinition( |
| PackageWarning.orphanedFile, |
| "orphaned-file", |
| "Dartdoc generated files that are unreachable from the index"), |
| PackageWarning.unknownFile: const PackageWarningDefinition( |
| PackageWarning.unknownFile, |
| "unknown-file", |
| "A leftover file exists in the tree that dartdoc did not write in this pass"), |
| PackageWarning.missingFromSearchIndex: const PackageWarningDefinition( |
| PackageWarning.missingFromSearchIndex, |
| "missing-from-search-index", |
| "A file generated by dartdoc is not present in the generated index.json"), |
| PackageWarning.typeAsHtml: const PackageWarningDefinition( |
| PackageWarning.typeAsHtml, |
| "type-as-html", |
| "Use of <> in a comment for type parameters is being treated as HTML by markdown", |
| defaultWarningMode: PackageWarningMode.ignore), |
| PackageWarning.invalidParameter: const PackageWarningDefinition( |
| PackageWarning.invalidParameter, |
| "invalid-parameter", |
| "A parameter given to a dartdoc directive was invalid.", |
| defaultWarningMode: PackageWarningMode.error), |
| PackageWarning.toolError: const PackageWarningDefinition( |
| PackageWarning.toolError, |
| "tool-error", |
| "Unable to execute external tool.", |
| defaultWarningMode: PackageWarningMode.error), |
| PackageWarning.deprecated: const PackageWarningDefinition( |
| PackageWarning.deprecated, |
| "deprecated", |
| "A dartdoc directive has a deprecated format."), |
| PackageWarning.unresolvedExport: const PackageWarningDefinition( |
| PackageWarning.unresolvedExport, |
| "unresolved-export", |
| "An export refers to a URI that cannot be resolved.", |
| defaultWarningMode: PackageWarningMode.error), |
| }; |
| |
| /// Something that package warnings can be called on. Optionally associated |
| /// with an analyzer [element]. |
| abstract class Warnable implements Canonicalization { |
| void warn(PackageWarning warning, |
| {String message, Iterable<Locatable> referredFrom}); |
| Element get element; |
| Warnable get enclosingElement; |
| Package get package; |
| } |
| |
| /// Something that can be located for warning purposes. |
| abstract class Locatable { |
| List<Locatable> get documentationFrom; |
| String get fullyQualifiedName; |
| String get href; |
| |
| /// A string indicating the URI of this Locatable, usually derived from |
| /// [Element.location]. |
| String get location; |
| } |
| |
| // The kinds of warnings that can be displayed when documenting a package. |
| enum PackageWarning { |
| ambiguousDocReference, |
| ambiguousReexport, |
| ignoredCanonicalFor, |
| noCanonicalFound, |
| noLibraryLevelDocs, |
| packageOrderGivesMissingPackageName, |
| reexportedPrivateApiAcrossPackages, |
| unresolvedDocReference, |
| unknownMacro, |
| unknownHtmlFragment, |
| brokenLink, |
| orphanedFile, |
| unknownFile, |
| missingFromSearchIndex, |
| typeAsHtml, |
| invalidParameter, |
| toolError, |
| deprecated, |
| unresolvedExport, |
| } |
| |
| /// Used to declare defaults for a particular package warning. |
| enum PackageWarningMode { |
| ignore, |
| warn, |
| error, |
| } |
| |
| /// Warnings it is OK to skip if we can determine the warnable isn't documented. |
| /// In particular, this set should not include warnings around public/private |
| /// or canonicalization problems, because those can break the isDocumented() |
| /// check. |
| final Set<PackageWarning> skipWarningIfNotDocumentedFor = new Set() |
| ..addAll([PackageWarning.unresolvedDocReference, PackageWarning.typeAsHtml]); |
| |
| class PackageWarningOptions { |
| final Map<PackageWarning, PackageWarningMode> warningModes = {}; |
| |
| PackageWarningOptions() { |
| for (PackageWarningDefinition definition |
| in packageWarningDefinitions.values) { |
| switch (definition.defaultWarningMode) { |
| case PackageWarningMode.warn: |
| { |
| warn(definition.kind); |
| } |
| break; |
| case PackageWarningMode.error: |
| { |
| error(definition.kind); |
| } |
| break; |
| case PackageWarningMode.ignore: |
| { |
| ignore(definition.kind); |
| } |
| break; |
| } |
| } |
| } |
| |
| /// [packageMeta] parameter is for testing. |
| static PackageWarningOptions fromOptions( |
| DartdocSyntheticOption<PackageWarningOptions> option, Directory dir) { |
| // First, initialize defaults. |
| PackageWarningOptions newOptions = PackageWarningOptions(); |
| PackageMeta packageMeta = new PackageMeta.fromDir(dir); |
| |
| // Interpret errors/warnings/ignore options. In the event of conflict, warning overrides error and |
| // ignore overrides warning. |
| for (String warningName in option.parent['errors'].valueAt(dir) ?? []) { |
| if (packageWarningsByName[warningName] != null) { |
| newOptions.error(packageWarningsByName[warningName].kind); |
| } |
| } |
| for (String warningName in option.parent['warnings'].valueAt(dir) ?? []) { |
| if (packageWarningsByName[warningName] != null) { |
| newOptions.warn(packageWarningsByName[warningName].kind); |
| } |
| } |
| for (String warningName in option.parent['ignore'].valueAt(dir) ?? []) { |
| if (packageWarningsByName[warningName] != null) { |
| newOptions.ignore(packageWarningsByName[warningName].kind); |
| } |
| } |
| |
| // Check whether warnings are allowed at all in this package. |
| List<String> allowWarningsInPackages = |
| option.parent['allowWarningsInPackages'].valueAt(dir); |
| List<String> allowErrorsInPackages = |
| option.parent['allowErrorsInPackages'].valueAt(dir); |
| List<String> ignoreWarningsInPackages = |
| option.parent['ignoreWarningsInPackages'].valueAt(dir); |
| List<String> ignoreErrorsInPackages = |
| option.parent['ignoreErrorsInPackages'].valueAt(dir); |
| if (allowWarningsInPackages != null && |
| !allowWarningsInPackages.contains(packageMeta.name)) { |
| PackageWarning.values |
| .forEach((PackageWarning kind) => newOptions.ignore(kind)); |
| } |
| if (allowErrorsInPackages != null && |
| !allowWarningsInPackages.contains(packageMeta.name)) { |
| PackageWarning.values |
| .forEach((PackageWarning kind) => newOptions.ignore(kind)); |
| } |
| if (ignoreWarningsInPackages != null && |
| ignoreWarningsInPackages.contains(packageMeta.name)) { |
| PackageWarning.values |
| .forEach((PackageWarning kind) => newOptions.ignore(kind)); |
| } |
| if (ignoreErrorsInPackages != null && |
| ignoreErrorsInPackages.contains(packageMeta.name)) { |
| PackageWarning.values |
| .forEach((PackageWarning kind) => newOptions.ignore(kind)); |
| } |
| return newOptions; |
| } |
| |
| void ignore(PackageWarning kind) => |
| warningModes[kind] = PackageWarningMode.ignore; |
| void warn(PackageWarning kind) => |
| warningModes[kind] = PackageWarningMode.warn; |
| void error(PackageWarning kind) => |
| warningModes[kind] = PackageWarningMode.error; |
| |
| PackageWarningMode getMode(PackageWarning kind) => warningModes[kind]; |
| } |
| |
| class PackageWarningCounter { |
| final countedWarnings = |
| new Map<Element, Set<Tuple2<PackageWarning, String>>>(); |
| final _items = <Jsonable>[]; |
| final _displayedWarningCounts = <PackageWarning, int>{}; |
| final PackageGraph packageGraph; |
| |
| PackageWarningCounter(this.packageGraph); |
| |
| /// Actually write out the warning. Assumes it is already counted with add. |
| void _writeWarning(PackageWarning kind, PackageWarningMode mode, |
| bool verboseWarnings, String name, String fullMessage) { |
| if (mode == PackageWarningMode.ignore) { |
| return; |
| } |
| String type; |
| if (mode == PackageWarningMode.error) { |
| type = "error"; |
| } else if (mode == PackageWarningMode.warn) { |
| type = "warning"; |
| } |
| if (type != null) { |
| var entry = " $type: $fullMessage"; |
| _displayedWarningCounts.putIfAbsent(kind, () => 0); |
| _displayedWarningCounts[kind] += 1; |
| if (_displayedWarningCounts[kind] == 1 && |
| verboseWarnings && |
| packageWarningDefinitions[kind].longHelp.isNotEmpty) { |
| // First time we've seen this warning. Give a little extra info. |
| final String separator = '\n '; |
| final String nameSub = r'@@name@@'; |
| String verboseOut = |
| '$separator${packageWarningDefinitions[kind].longHelp.join(separator)}' |
| .replaceAll(nameSub, name); |
| entry = '$entry$verboseOut'; |
| } |
| assert(entry == entry.trimRight()); |
| _items.add(new _JsonWarning(type, kind, fullMessage, entry)); |
| } |
| for (var item in _items) { |
| logWarning(item); |
| } |
| _items.clear(); |
| } |
| |
| /// Returns true if we've already warned for this. |
| bool hasWarning(Warnable element, PackageWarning kind, String message) { |
| Tuple2<PackageWarning, String> warningData = new Tuple2(kind, message); |
| if (countedWarnings.containsKey(element?.element)) { |
| return countedWarnings[element?.element].contains(warningData); |
| } |
| return false; |
| } |
| |
| /// Adds the warning to the counter, and writes out the fullMessage string |
| /// if configured to do so. |
| void addWarning(Warnable element, PackageWarning kind, String message, |
| String fullMessage) { |
| assert(!hasWarning(element, kind, message)); |
| // TODO(jcollins-g): Make addWarning not accept nulls for element. |
| PackageWarningOptionContext config = |
| element?.config ?? packageGraph.defaultPackage.config; |
| PackageWarningMode warningMode = config.packageWarningOptions.getMode(kind); |
| if (!config.allowNonLocalWarnings && |
| element != null && |
| !element.package.isLocal) { |
| warningMode = PackageWarningMode.ignore; |
| } |
| if (warningMode == PackageWarningMode.warn) |
| warningCount += 1; |
| else if (warningMode == PackageWarningMode.error) errorCount += 1; |
| Tuple2<PackageWarning, String> warningData = new Tuple2(kind, message); |
| countedWarnings.putIfAbsent(element?.element, () => new Set()); |
| countedWarnings[element?.element].add(warningData); |
| _writeWarning(kind, warningMode, config.verboseWarnings, |
| element?.fullyQualifiedName, fullMessage); |
| } |
| |
| int errorCount = 0; |
| int warningCount = 0; |
| |
| @override |
| String toString() { |
| String errors = '$errorCount ${errorCount == 1 ? "error" : "errors"}'; |
| String warnings = |
| '$warningCount ${warningCount == 1 ? "warning" : "warnings"}'; |
| return [errors, warnings].join(', '); |
| } |
| } |
| |
| class _JsonWarning extends Jsonable { |
| final String type; |
| final PackageWarning kind; |
| final String message; |
| |
| @override |
| final String text; |
| |
| _JsonWarning(this.type, this.kind, this.message, this.text); |
| |
| @override |
| Map<String, dynamic> toJson() => { |
| 'type': type, |
| 'kind': packageWarningDefinitions[kind].warningName, |
| 'message': message, |
| 'text': text |
| }; |
| } |