blob: de96ee860cc9cf3b8f55cb1f2f56df3f729348d5 [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 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/logging.dart';
import 'package:dartdoc/src/model/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<Object>>> createPackageWarningOptions(
PackageMetaProvider packageMetaProvider,
) async {
var resourceProvider = packageMetaProvider.resourceProvider;
return [
DartdocOptionArgOnly<bool>('allowNonLocalWarnings', false, resourceProvider,
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.
DartdocOptionArgOnly<List<String>>(
'allowWarningsInPackages', null, resourceProvider,
help:
'Package names to display warnings for (ignore all others if set).'),
DartdocOptionArgOnly<List<String>>(
'allowErrorsInPackages', null, resourceProvider,
help: 'Package names to display errors for (ignore all others if set)'),
DartdocOptionArgOnly<List<String>>(
'ignoreWarningsInPackages', null, resourceProvider,
help: 'Package names to ignore warnings for. Takes priority over '
'allow-warnings-in-packages'),
DartdocOptionArgOnly<List<String>>(
'ignoreErrorsInPackages', null, resourceProvider,
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.
DartdocOptionArgFile<List<String>>('errors', null, resourceProvider,
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')),
DartdocOptionArgFile<List<String>>('ignore', null, resourceProvider,
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')),
DartdocOptionArgFile<List<String>>('warnings', null, resourceProvider,
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.
DartdocOptionSyntheticOnly<PackageWarningOptions>(
'packageWarningOptions',
(DartdocSyntheticOption<PackageWarningOptions> option, Folder dir) =>
PackageWarningOptions.fromOptions(option, dir, packageMetaProvider),
resourceProvider,
),
];
}
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})
: longHelp = longHelp ?? const [],
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 =
Map.fromEntries(packageWarningDefinitions.values
.map((definition) => 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: PackageWarningDefinition(
PackageWarning.ambiguousDocReference,
'ambiguous-doc-reference',
'A comment reference could refer to two or more different objects'),
PackageWarning.ambiguousReexport: 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: [
"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: PackageWarningDefinition(
PackageWarning.ignoredCanonicalFor,
'ignored-canonical-for',
'A @canonicalFor tag refers to a library which this symbol can not be '
'canonical for'),
PackageWarning.noCanonicalFound: 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.noDefiningLibraryFound: PackageWarningDefinition(
PackageWarning.noDefiningLibraryFound,
'no-defining-library-found',
'The defining library for an element could not be found; the library may '
'be imported or exported with a non-standard URI',
defaultWarningMode: PackageWarningMode.error),
PackageWarning.notImplemented: PackageWarningDefinition(
PackageWarning.notImplemented,
'not-implemented',
'The code makes use of a feature that is not yet implemented in dartdoc'),
PackageWarning.noDocumentableLibrariesInPackage: PackageWarningDefinition(
PackageWarning.noDocumentableLibrariesInPackage,
'no-documentable-libraries',
'The package is to be documented but has no Dart libraries to document',
longHelp: [
'Dartdoc could not find any public libraries to document in @@name@@, ',
'but documentation was requested. This might be expected for an ',
'asset only package, in which case, disable this warning in your ',
'dartdoc_options.yaml file.',
],
),
PackageWarning.noLibraryLevelDocs: PackageWarningDefinition(
PackageWarning.noLibraryLevelDocs,
'no-library-level-docs',
'There are no library level docs for this library'),
PackageWarning.packageOrderGivesMissingPackageName: 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: PackageWarningDefinition(
PackageWarning.reexportedPrivateApiAcrossPackages,
'reexported-private-api-across-packages',
'One or more libraries reexports private API members from outside its own package'),
PackageWarning.unresolvedDocReference: 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: PackageWarningDefinition(PackageWarning.brokenLink,
'broken-link', 'Dartdoc generated a link to a non-existent file'),
PackageWarning.unknownDirective: PackageWarningDefinition(
PackageWarning.unknownDirective,
'unknown-directive',
'A comment contains an unknown directive'),
PackageWarning.unknownMacro: PackageWarningDefinition(
PackageWarning.unknownMacro,
'unknown-macro',
'A comment reference contains an unknown macro'),
PackageWarning.orphanedFile: PackageWarningDefinition(
PackageWarning.orphanedFile,
'orphaned-file',
'Dartdoc generated files that are unreachable from the index'),
PackageWarning.unknownFile: PackageWarningDefinition(
PackageWarning.unknownFile,
'unknown-file',
'A leftover file exists in the tree that dartdoc did not write in this pass'),
PackageWarning.missingFromSearchIndex: PackageWarningDefinition(
PackageWarning.missingFromSearchIndex,
'missing-from-search-index',
'A file generated by dartdoc is not present in the generated index.json'),
PackageWarning.typeAsHtml: 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: PackageWarningDefinition(
PackageWarning.invalidParameter,
'invalid-parameter',
'A parameter given to a dartdoc directive was invalid.',
defaultWarningMode: PackageWarningMode.error),
PackageWarning.toolError: PackageWarningDefinition(PackageWarning.toolError,
'tool-error', 'Unable to execute external tool.',
defaultWarningMode: PackageWarningMode.error),
PackageWarning.deprecated: PackageWarningDefinition(PackageWarning.deprecated,
'deprecated', 'A dartdoc directive has a deprecated format.'),
PackageWarning.unresolvedExport: PackageWarningDefinition(
PackageWarning.unresolvedExport,
'unresolved-export',
'An export refers to a URI that cannot be resolved.',
defaultWarningMode: PackageWarningMode.error),
PackageWarning.duplicateFile: PackageWarningDefinition(
PackageWarning.duplicateFile,
'duplicate-file',
'Dartdoc is trying to write to a duplicate filename based on the names of Dart symbols.',
longHelp: [
'Dartdoc generates a path and filename to write to for each symbol.',
'@@name@@ conflicts with another symbol in the generated path, and',
'therefore can not be written out. Changing the name, library name, or',
'class name (if appropriate) of one of the conflicting items can resolve',
"the conflict. Alternatively, use the @nodoc tag in one symbol's",
'documentation comments to hide it.'
],
defaultWarningMode: PackageWarningMode.error),
PackageWarning.missingConstantConstructor: PackageWarningDefinition(
PackageWarning.missingConstantConstructor,
'missing-constant-constructor',
'Dartdoc can not show the value of a constant because its constructor could not be resolved.',
longHelp: [
'To resolve a constant into its literal value, Dartdoc relies on the',
"analyzer to resolve the constructor. The analyzer didn't provide",
'the constructor for @@name@@, which is usually due to an error in the',
'code. Use the analyzer to find missing imports.',
],
// Defaults to ignore as this doesn't impact the docs severely but is
// useful for debugging package structure.
defaultWarningMode: PackageWarningMode.ignore),
};
/// 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;
}
// The kinds of warnings that can be displayed when documenting a package.
enum PackageWarning {
ambiguousDocReference,
ambiguousReexport,
ignoredCanonicalFor,
noCanonicalFound,
noDefiningLibraryFound,
notImplemented,
noDocumentableLibrariesInPackage,
noLibraryLevelDocs,
packageOrderGivesMissingPackageName,
reexportedPrivateApiAcrossPackages,
unresolvedDocReference,
unknownDirective,
unknownMacro,
unknownHtmlFragment,
brokenLink,
duplicateFile,
orphanedFile,
unknownFile,
missingFromSearchIndex,
typeAsHtml,
invalidParameter,
toolError,
deprecated,
unresolvedExport,
missingConstantConstructor,
missingExampleFile,
}
/// 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 = {
PackageWarning.unresolvedDocReference,
PackageWarning.typeAsHtml
};
class PackageWarningOptions {
final Map<PackageWarning, PackageWarningMode> warningModes = {};
PackageWarningOptions() {
for (var 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;
}
}
}
static PackageWarningOptions fromOptions(
DartdocSyntheticOption<PackageWarningOptions> option,
Folder dir,
PackageMetaProvider packageMetaProvider,
) {
// First, initialize defaults.
var newOptions = PackageWarningOptions();
var packageMeta = packageMetaProvider.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 Map<Element, Set<Tuple2<PackageWarning, String>>> countedWarnings = {};
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 separator = '\n ';
final nameSub = r'@@name@@';
var verboseOut =
'$separator${packageWarningDefinitions[kind].longHelp.join(separator)}'
.replaceAll(nameSub, name);
entry = '$entry$verboseOut';
}
assert(entry == entry.trimRight());
_items.add(_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) {
var warningData = Tuple2<PackageWarning, String>(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;
var warningMode = config.packageWarningOptions.getMode(kind);
if (!config.allowNonLocalWarnings && !(element?.package?.isLocal ?? true)) {
warningMode = PackageWarningMode.ignore;
}
if (warningMode == PackageWarningMode.warn) {
warningCount += 1;
} else if (warningMode == PackageWarningMode.error) {
errorCount += 1;
}
var warningData = Tuple2<PackageWarning, String>(kind, message);
countedWarnings.putIfAbsent(element?.element, () => {}).add(warningData);
_writeWarning(kind, warningMode, config.verboseWarnings,
element?.fullyQualifiedName, fullMessage);
}
int errorCount = 0;
int warningCount = 0;
@override
String toString() {
var errors = '$errorCount ${errorCount == 1 ? "error" : "errors"}';
var 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
};
}