blob: 1ac42e8a4a843ce460c01befeab5d138a612e04c [file] [log] [blame]
// 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
};
}