blob: 2f409fbe4e5ce412a1b3c7776b01f0452ce26caf [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.
///
/// dartdoc's dartdoc_options.yaml configuration file follows similar loading
/// semantics to that of analysis_options.yaml,
/// [documented here](https://www.dartlang.org/guides/language/analysis-options).
/// It searches parent directories until it finds an analysis_options.yaml file,
/// and uses built-in defaults if one is not found.
///
/// The classes here manage both the dartdoc_options.yaml loading and command
/// line arguments.
///
library dartdoc.dartdoc_options;
import 'dart:async';
import 'dart:io';
import 'package:analyzer/dart/element/element.dart';
import 'package:args/args.dart';
import 'package:dartdoc/dartdoc.dart';
import 'package:dartdoc/src/io_utils.dart';
import 'package:dartdoc/src/tuple.dart';
import 'package:path/path.dart' as pathLib;
import 'package:yaml/yaml.dart';
/// Constants to help with type checking, because T is int and so forth
/// don't work in Dart.
const String _kStringVal = '';
const List<String> _kListStringVal = const <String>[];
const Map<String, String> _kMapStringVal = const <String, String>{};
const int _kIntVal = 0;
const double _kDoubleVal = 0.0;
const bool _kBoolVal = true;
/// Args are computed relative to the current directory at the time the
/// program starts.
final Directory directoryCurrent = Directory.current;
final String directoryCurrentPath =
pathLib.canonicalize(Directory.current.path);
class DartdocOptionError extends DartdocFailure {
DartdocOptionError(String details) : super(details);
}
class DartdocFileMissing extends DartdocOptionError {
DartdocFileMissing(String details) : super(details);
}
/// Defines the attributes of a category in the options file, corresponding to
/// the 'categories' keyword in the options file, and populated by the
/// [CategoryConfiguration] class.
class CategoryDefinition {
/// Internal name of the category.
final String name;
/// Displayed name of the category in docs, or null if there is none.
final String _displayName;
/// Canonical path of the markdown file used to document this category
/// (or null if undocumented).
final String documentationMarkdown;
CategoryDefinition(this.name, this._displayName, this.documentationMarkdown);
/// Returns the [_displayName], if available, or else simply [name].
String get displayName => _displayName ?? name;
}
/// A configuration class that can interpret category definitions from a YAML
/// map.
class CategoryConfiguration {
/// A map of [CategoryDefinition.name] to [CategoryDefinition] objects.
final Map<String, CategoryDefinition> categoryDefinitions;
CategoryConfiguration._(this.categoryDefinitions);
static CategoryConfiguration get empty {
return new CategoryConfiguration._({});
}
static CategoryConfiguration fromYamlMap(
YamlMap yamlMap, pathLib.Context pathContext) {
Map<String, CategoryDefinition> newCategoryDefinitions = {};
for (MapEntry entry in yamlMap.entries) {
String name = entry.key.toString();
String displayName;
String documentationMarkdown;
var categoryMap = entry.value;
if (categoryMap is Map) {
displayName = categoryMap['displayName']?.toString();
documentationMarkdown = categoryMap['markdown']?.toString();
if (documentationMarkdown != null) {
documentationMarkdown =
pathContext.canonicalize(documentationMarkdown);
if (!new File(documentationMarkdown).existsSync()) {
throw new DartdocFileMissing(
'In categories definition for ${name}, "markdown" resolves to the missing file $documentationMarkdown');
}
}
newCategoryDefinitions[name] =
new CategoryDefinition(name, displayName, documentationMarkdown);
}
}
return new CategoryConfiguration._(newCategoryDefinitions);
}
}
/// Defines the attributes of a tool in the options file, corresponding to
/// the 'tools' keyword in the options file, and populated by the
/// [ToolConfiguration] class.
class ToolDefinition {
/// A list containing the command and options to be run for this tool. The
/// first argument in the command is the tool executable, and will have its
/// path evaluated relative to the `dartdoc_options.yaml` location. Must not
/// be an empty list, or be null.
final List<String> command;
/// A list containing the command and options to setup phase for this tool.
/// The first argument in the command is the tool executable, and will have
/// its path evaluated relative to the `dartdoc_options.yaml` location. May
/// be null or empty, in which case it will be ignored at setup time.
final List<String> setupCommand;
/// A description of the defined tool. Must not be null.
final String description;
/// If set, then the setup command has been run once for this tool definition.
bool setupComplete = false;
/// Returns true if the given executable path has an extension recognized as a
/// Dart extension (e.g. '.dart' or '.snapshot').
static bool isDartExecutable(String executable) {
var extension = pathLib.extension(executable);
return extension == '.dart' || extension == '.snapshot';
}
/// Creates a ToolDefinition or subclass that is appropriate for the command
/// given.
factory ToolDefinition.fromCommand(
List<String> command, List<String> setupCommand, String description) {
assert(command != null);
assert(command.isNotEmpty);
assert(description != null);
if (isDartExecutable(command[0])) {
return DartToolDefinition(command, setupCommand, description);
} else {
return ToolDefinition(command, setupCommand, description);
}
}
ToolDefinition(this.command, this.setupCommand, this.description)
: assert(command != null),
assert(command.isNotEmpty),
assert(description != null);
@override
String toString() {
final String commandString =
'${this is DartToolDefinition ? '(Dart) ' : ''}"${command.join(' ')}"';
if (setupCommand == null) {
return '$runtimeType: $commandString ($description)';
} else {
return '$runtimeType: $commandString, with setup command '
'"${setupCommand.join(' ')}" ($description)';
}
}
}
/// Manages the creation of a single snapshot file in a context where multiple
/// async functions could be trying to use and/or create it.
///
/// To use:
///
/// var s = new Snapshot(...);
///
/// if (s.needsSnapshot) {
/// // create s.snapshotFile, then call:
/// s.snapshotCompleted();
/// } else {
/// await snapshotValid();
/// // use existing s.snapshotFile;
/// }
///
class Snapshot {
File _snapshotFile;
File get snapshotFile => _snapshotFile;
final Completer _snapshotCompleter = Completer();
Snapshot(Directory snapshotCache, String toolPath, int serial) {
if (toolPath.endsWith('.snapshot')) {
_needsSnapshot = false;
_snapshotFile = File(toolPath);
snapshotCompleted();
} else {
_snapshotFile =
File(pathLib.join(snapshotCache.absolute.path, 'snapshot_$serial'));
}
}
bool _needsSnapshot = true;
/// Will return true precisely once, unless [toolPath] was already a snapshot.
/// In that case, will always return false.
bool get needsSnapshot {
if (_needsSnapshot == true) {
_needsSnapshot = false;
return true;
}
return _needsSnapshot;
}
Future<void> snapshotValid() => _snapshotCompleter.future;
void snapshotCompleted() => _snapshotCompleter.complete();
}
/// A singleton that keeps track of cached snapshot files. The [dispose]
/// function must be called before process exit to clean up snapshots in the
/// cache.
class SnapshotCache {
static SnapshotCache _instance;
Directory snapshotCache;
final Map<String, Snapshot> snapshots = {};
int _serial = 0;
SnapshotCache._()
: snapshotCache =
Directory.systemTemp.createTempSync('dartdoc_snapshot_cache_');
static SnapshotCache get instance {
_instance ??= SnapshotCache._();
return _instance;
}
Snapshot getSnapshot(String toolPath) {
if (snapshots.containsKey(toolPath)) {
return snapshots[toolPath];
}
snapshots[toolPath] = new Snapshot(snapshotCache, toolPath, _serial);
_serial++;
return snapshots[toolPath];
}
void dispose() {
if (snapshotCache != null && snapshotCache.existsSync()) {
snapshotCache.deleteSync(recursive: true);
}
_instance = null;
}
}
/// A special kind of tool definition for Dart commands.
class DartToolDefinition extends ToolDefinition {
/// Takes a list of args to modify, and returns the name of the executable
/// to run. If no snapshot file existed, then create one and modify the args
/// so that if they are executed with dart, will result in the snapshot being
/// built.
Future<Tuple2<String, Function()>> modifyArgsToCreateSnapshotIfNeeded(
List<String> args) async {
assert(args[0] == command.first);
// Set up flags to create a new snapshot, if needed, and use the first run as the training
// run.
Snapshot snapshot = SnapshotCache.instance.getSnapshot(command.first);
File snapshotFile = snapshot.snapshotFile;
bool needsSnapshot = snapshot.needsSnapshot;
if (needsSnapshot) {
args.insertAll(0, [
'--snapshot=${snapshotFile.absolute.path}',
'--snapshot_kind=app-jit'
]);
} else {
await snapshot.snapshotValid();
// replace the first argument with the path to the snapshot.
args[0] = snapshotFile.absolute.path;
}
return new Tuple2(Platform.resolvedExecutable,
needsSnapshot ? snapshot.snapshotCompleted : null);
}
DartToolDefinition(
List<String> command, List<String> setupCommand, String description)
: super(command, setupCommand, description);
}
/// A configuration class that can interpret [ToolDefinition]s from a YAML map.
class ToolConfiguration {
final Map<String, ToolDefinition> tools;
ToolConfiguration._(this.tools);
static ToolConfiguration get empty {
return new ToolConfiguration._({});
}
static ToolConfiguration fromYamlMap(
YamlMap yamlMap, pathLib.Context pathContext) {
var newToolDefinitions = <String, ToolDefinition>{};
for (var entry in yamlMap.entries) {
var name = entry.key.toString();
var toolMap = entry.value;
var description;
List<String> command;
List<String> setupCommand;
if (toolMap is Map) {
description = toolMap['description']?.toString();
List<String> findCommand([String prefix = '']) {
List<String> command;
// If the command key is given, then it applies to all platforms.
var commandFrom = toolMap.containsKey('${prefix}command')
? '${prefix}command'
: '$prefix${Platform.operatingSystem}';
if (toolMap.containsKey(commandFrom)) {
if (toolMap[commandFrom].value is String) {
command = [toolMap[commandFrom].toString()];
if (command[0].isEmpty) {
throw new DartdocOptionError(
'Tool commands must not be empty. Tool $name command entry '
'"$commandFrom" must contain at least one path.');
}
} else if (toolMap[commandFrom] is YamlList) {
command = (toolMap[commandFrom] as YamlList)
.map<String>((node) => node.toString())
.toList();
if (command.isEmpty) {
throw new DartdocOptionError(
'Tool commands must not be empty. Tool $name command entry '
'"$commandFrom" must contain at least one path.');
}
} else {
throw new DartdocOptionError(
'Tool commands must be a path to an executable, or a list of '
'strings that starts with a path to an executable. '
'The tool $name has a $commandFrom entry that is a '
'${toolMap[commandFrom].runtimeType}');
}
}
return command;
}
command = findCommand();
setupCommand = findCommand('setup_');
} else {
throw new DartdocOptionError(
'Tools must be defined as a map of tool names to definitions. Tool '
'$name is not a map.');
}
if (command == null) {
throw new DartdocOptionError(
'At least one of "command" or "${Platform.operatingSystem}" must '
'be defined for the tool $name.');
}
bool validateExecutable(String executable) {
bool isExecutable(int mode) {
return (0x1 & ((mode >> 6) | (mode >> 3) | mode)) != 0;
}
var executableFile = new File(executable);
var exeStat = executableFile.statSync();
if (exeStat.type == FileSystemEntityType.notFound) {
throw new DartdocOptionError('Command executables must exist. '
'The file "$executable" does not exist for tool $name.');
}
var isDartCommand = ToolDefinition.isDartExecutable(executable);
// Dart scripts don't need to be executable, because they'll be
// executed with the Dart binary.
if (!isDartCommand && !isExecutable(exeStat.mode)) {
throw new DartdocOptionError('Non-Dart commands must be '
'executable. The file "$executable" for tool $name does not have '
'execute permission.');
}
return isDartCommand;
}
var executable = pathContext.canonicalize(command.removeAt(0));
validateExecutable(executable);
if (setupCommand != null) {
var setupExecutable =
pathContext.canonicalize(setupCommand.removeAt(0));
var isDartSetupCommand = validateExecutable(executable);
// Setup commands aren't snapshotted, since they're only run once.
setupCommand = (isDartSetupCommand
? [Platform.resolvedExecutable, setupExecutable]
: [setupExecutable]) +
setupCommand;
}
newToolDefinitions[name] = new ToolDefinition.fromCommand(
[executable] + command, setupCommand, description);
}
return new ToolConfiguration._(newToolDefinitions);
}
}
/// A container class to keep track of where our yaml data came from.
class _YamlFileData {
/// The map from the yaml file.
final Map data;
/// The path to the directory containing the yaml file.
final String canonicalDirectoryPath;
_YamlFileData(this.data, this.canonicalDirectoryPath);
}
/// Some DartdocOption subclasses need to keep track of where they
/// got the value from; this class contains those intermediate results
/// so that error messages can be more useful.
class _OptionValueWithContext<T> {
/// The value of the option at canonicalDirectoryPath.
final T value;
/// A canonical path to the directory where this value came from. May
/// be different from [DartdocOption.valueAt]'s `dir` parameter.
String canonicalDirectoryPath;
/// If non-null, the basename of the configuration file the value came from.
String definingFile;
/// A [pathLib.Context] variable initialized with canonicalDirectoryPath.
pathLib.Context pathContext;
/// Build a _OptionValueWithContext.
/// [path] is the path where this value came from (not required to be canonical)
_OptionValueWithContext(this.value, String path, {String definingFile}) {
this.definingFile = definingFile;
canonicalDirectoryPath = pathLib.canonicalize(path);
pathContext = new pathLib.Context(current: canonicalDirectoryPath);
}
/// Assume value is a path, and attempt to resolve it. Throws [UnsupportedError]
/// if [T] isn't a [String] or [List<String>].
T get resolvedValue {
if (value is List<String>) {
return (value as List<String>)
.map((v) => pathContext.canonicalize(resolveTildePath(v)))
.cast<String>()
.toList() as T;
} else if (value is String) {
return pathContext.canonicalize(resolveTildePath(value as String)) as T;
} else if (value is Map<String, String>) {
return (value as Map<String, String>)
.map<String, String>((String key, String value) {
return new MapEntry(
key, pathContext.canonicalize(resolveTildePath(value)));
}) as T;
} else {
throw new UnsupportedError('Type $T is not supported for resolvedValue');
}
}
}
/// An abstract class for interacting with dartdoc options.
///
/// This class and its implementations allow Dartdoc to declare options
/// that are both defined in a configuration file and specified via the
/// command line, with searching the directory tree for a proper file
/// and overriding file options with the command line built-in. A number
/// of sanity checks are also built in to these classes so that file existence
/// can be verified, types constrained, and defaults provided.
///
/// Use via implementations [DartdocOptionSet], [DartdocOptionArgFile],
/// [DartdocOptionArgOnly], and [DartdocOptionFileOnly].
abstract class DartdocOption<T> {
/// This is the value returned if we couldn't find one otherwise.
final T defaultsTo;
/// Text string for help passed on in command line options.
final String help;
/// The name of this option, not including the names of any parents.
final String name;
/// Set to true if this option represents the name of a directory.
final bool isDir;
/// Set to true if this option represents the name of a file.
final bool isFile;
/// Set to true if DartdocOption subclasses should validate that the
/// directory or file exists. Does not imply validation of [defaultsTo],
/// and requires that one of [isDir] or [isFile] is set.
final bool mustExist;
DartdocOption._(this.name, this.defaultsTo, this.help, this.isDir,
this.isFile, this.mustExist, this._convertYamlToType) {
assert(!(isDir && isFile));
if (isDir || isFile) assert(_isString || _isListString || _isMapString);
if (mustExist) {
assert(isDir || isFile);
}
}
/// Closure to convert yaml data into some other structure.
T Function(YamlMap, pathLib.Context) _convertYamlToType;
// The choice not to use reflection means there's some ugly type checking,
// somewhat more ugly than we'd have to do anyway to automatically convert
// command line arguments and yaml data to real types.
//
// Condense the ugly all in one place, this set of getters.
bool get _isString => _kStringVal is T;
bool get _isListString => _kListStringVal is T;
bool get _isMapString => _kMapStringVal is T;
bool get _isBool => _kBoolVal is T;
bool get _isInt => _kIntVal is T;
bool get _isDouble => _kDoubleVal is T;
DartdocOption _parent;
/// The parent of this DartdocOption, or null if this is the root.
DartdocOption get parent => _parent;
final Map<String, _YamlFileData> __yamlAtCanonicalPathCache = {};
/// Implementation detail for [DartdocOptionFileOnly]. Make sure we use
/// the root node's cache.
Map<String, _YamlFileData> get _yamlAtCanonicalPathCache =>
root.__yamlAtCanonicalPathCache;
final ArgParser __argParser = new ArgParser();
ArgParser get argParser => root.__argParser;
ArgResults __argResults;
/// Parse these as string arguments (from argv) with the argument parser.
/// Call before calling [valueAt] for any [DartdocOptionArgOnly] or
/// [DartdocOptionArgFile] in this tree.
void _parseArguments(List<String> arguments) {
__argResults = argParser.parse(arguments);
}
/// Throw [DartdocFileMissing] with a detailed error message indicating where
/// the error came from when a file or directory option is missing.
void _onMissing(
_OptionValueWithContext valueWithContext, String missingFilename);
/// Call [_onMissing] for every path that does not exist.
void _validatePaths(_OptionValueWithContext valueWithContext) {
if (!mustExist) return;
assert(isDir || isFile);
List<String> resolvedPaths;
if (valueWithContext.value is String) {
resolvedPaths = [valueWithContext.resolvedValue];
} else if (valueWithContext.value is List<String>) {
resolvedPaths = valueWithContext.resolvedValue.toList();
} else if (valueWithContext.value is Map<String, String>) {
resolvedPaths = valueWithContext.resolvedValue.values.toList();
} else {
assert(
false,
"Trying to ensure existence of unsupported type "
"${valueWithContext.value.runtimeType}");
}
for (String path in resolvedPaths) {
FileSystemEntity f = isDir ? new Directory(path) : new File(path);
if (!f.existsSync()) {
_onMissing(valueWithContext, path);
}
}
}
/// For a [List<String>] or [String] value, if [isDir] or [isFile] is set,
/// resolve paths in value relative to canonicalPath.
T _handlePathsInContext(_OptionValueWithContext valueWithContext) {
if (valueWithContext?.value == null || !(isDir || isFile))
return valueWithContext?.value;
_validatePaths(valueWithContext);
return valueWithContext.resolvedValue;
}
/// Call this with argv to set up the argument overrides. Applies to all
/// children.
void parseArguments(List<String> arguments) =>
root._parseArguments(arguments);
ArgResults get _argResults => root.__argResults;
/// Set the parent of this [DartdocOption]. Do not call more than once.
set parent(DartdocOption newParent) {
assert(_parent == null);
_parent = newParent;
}
/// The root [DartdocOption] containing this object, or [this] if the object
/// has no parent.
DartdocOption get root {
DartdocOption p = this;
while (p.parent != null) {
p = p.parent;
}
return p;
}
/// All object names starting at the root.
Iterable<String> get keys {
List<String> keyList = [];
DartdocOption option = this;
while (option?.name != null) {
keyList.add(option.name);
option = option.parent;
}
return keyList.reversed;
}
/// Direct children of this node, mapped by name.
final Map<String, DartdocOption> _children = {};
/// Return the calculated value of this option, given the directory as context.
///
/// If [isFile] or [isDir] is set, the returned value will be transformed
/// into a canonical path relative to the current working directory
/// (for arguments) or the config file from which the value was derived.
///
/// May throw [DartdocOptionError] if a command line argument is of the wrong
/// type. If [mustExist] is true, will throw [DartdocFileMissing] for command
/// line parameters and file paths in config files that don't point to
/// corresponding files or directories.
T valueAt(Directory dir);
/// Calls [valueAt] with the working directory at the start of the program.
T valueAtCurrent() => valueAt(directoryCurrent);
/// Calls [valueAt] on the directory this element is defined in.
T valueAtElement(Element element) => valueAt(new Directory(
pathLib.canonicalize(pathLib.basename(element.source.fullName))));
/// Adds a DartdocOption to the children of this DartdocOption.
void add(DartdocOption option) {
if (_children.containsKey(option.name))
throw new DartdocOptionError(
'Tried to add two children with the same name: ${option.name}');
_children[option.name] = option;
option.parent = this;
option.traverse((option) => option._onAdd());
}
/// This method is guaranteed to be called when [this] or any parent is added.
void _onAdd() {}
/// Adds a list of dartdoc options to the children of this DartdocOption.
void addAll(Iterable<DartdocOption> options) =>
options.forEach((o) => add(o));
/// Get the immediate child of this node named [name].
DartdocOption operator [](String name) {
return _children[name];
}
/// Apply the function [visit] to [this] and all children.
void traverse(void visit(DartdocOption)) {
visit(this);
_children.values.forEach((d) => d.traverse(visit));
}
}
/// A class that defaults to a value computed from a closure, but can be
/// overridden by a file.
class DartdocOptionFileSynth<T> extends DartdocOption<T>
with DartdocSyntheticOption<T>, _DartdocFileOption<T> {
bool _parentDirOverridesChild;
@override
T Function(DartdocSyntheticOption<T>, Directory) _compute;
DartdocOptionFileSynth(String name, this._compute,
{bool mustExist = false,
String help = '',
bool isDir = false,
bool isFile = false,
bool parentDirOverridesChild,
T Function(YamlMap, pathLib.Context) convertYamlToType})
: super._(name, null, help, isDir, isFile, mustExist, convertYamlToType) {
_parentDirOverridesChild = parentDirOverridesChild;
}
@override
T valueAt(Directory dir) {
_OptionValueWithContext result = _valueAtFromFile(dir);
if (result?.definingFile != null) {
return _handlePathsInContext(result);
}
return _valueAtFromSynthetic(dir);
}
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingPath) {
if (valueWithContext.definingFile != null) {
_onMissingFromFiles(valueWithContext, missingPath);
} else {
_onMissingFromSynthetic(valueWithContext, missingPath);
}
}
@override
bool get parentDirOverridesChild => _parentDirOverridesChild;
}
/// A class that defaults to a value computed from a closure, but can
/// be overridden on the command line.
class DartdocOptionArgSynth<T> extends DartdocOption<T>
with DartdocSyntheticOption<T>, _DartdocArgOption<T> {
String _abbr;
bool _hide;
bool _negatable;
bool _splitCommas;
@override
T Function(DartdocSyntheticOption<T>, Directory) _compute;
DartdocOptionArgSynth(String name, this._compute,
{String abbr,
bool mustExist = false,
String help = '',
bool hide = false,
bool isDir = false,
bool isFile = false,
bool negatable = false,
bool splitCommas})
: super._(name, null, help, isDir, isFile, mustExist, null) {
_hide = hide;
_negatable = negatable;
_splitCommas = splitCommas;
_abbr = abbr;
}
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingPath) {
_onMissingFromArgs(valueWithContext, missingPath);
}
@override
T valueAt(Directory dir) {
if (_argResults.wasParsed(argName)) {
return _valueAtFromArgs();
}
return _valueAtFromSynthetic(dir);
}
@override
String get abbr => _abbr;
@override
bool get hide => _hide;
@override
bool get negatable => _negatable;
@override
bool get splitCommas => _splitCommas;
}
/// A synthetic option takes a closure at construction time that computes
/// the value of the configuration option based on other configuration options.
/// Does not protect against closures that self-reference. If [mustExist] and
/// [isDir] or [isFile] is set, computed values will be resolved to canonical
/// paths.
class DartdocOptionSyntheticOnly<T> extends DartdocOption<T>
with DartdocSyntheticOption<T> {
@override
T Function(DartdocSyntheticOption<T>, Directory) _compute;
DartdocOptionSyntheticOnly(String name, this._compute,
{bool mustExist = false,
String help = '',
bool isDir = false,
bool isFile = false})
: super._(name, null, help, isDir, isFile, mustExist, null);
}
abstract class DartdocSyntheticOption<T> implements DartdocOption<T> {
T Function(DartdocSyntheticOption<T>, Directory) get _compute;
@override
T valueAt(Directory dir) => _valueAtFromSynthetic(dir);
T _valueAtFromSynthetic(Directory dir) {
_OptionValueWithContext context =
new _OptionValueWithContext<T>(_compute(this, dir), dir.path);
return _handlePathsInContext(context);
}
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingPath) =>
_onMissingFromSynthetic(valueWithContext, missingPath);
void _onMissingFromSynthetic(
_OptionValueWithContext valueWithContext, String missingPath) {
String description =
'Synthetic configuration option ${name} from <internal>';
throw new DartdocFileMissing(
'$description, computed as ${valueWithContext.value}, resolves to missing path: "${missingPath}"');
}
}
typedef Future<List<DartdocOption>> OptionGenerator();
/// A [DartdocOption] that only contains other [DartdocOption]s and is not an option itself.
class DartdocOptionSet extends DartdocOption<Null> {
DartdocOptionSet(String name)
: super._(name, null, null, false, false, false, null);
/// Asynchronous factory that is the main entry point to initialize Dartdoc
/// options for use.
///
/// [name] is the top level key for the option set.
/// [optionGenerators] is a sequence of asynchronous functions that return
/// [DartdocOption]s that will be added to the new option set.
static Future<DartdocOptionSet> fromOptionGenerators(
String name, Iterable<OptionGenerator> optionGenerators) async {
DartdocOptionSet optionSet = new DartdocOptionSet(name);
for (OptionGenerator generator in optionGenerators) {
optionSet.addAll(await generator());
}
return optionSet;
}
/// [DartdocOptionSet] always has the null value.
@override
Null valueAt(Directory dir) => null;
/// Since we have no value, [_onMissing] does nothing.
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingFilename) {}
/// Traverse skips this node, because it doesn't represent a real configuration object.
@override
void traverse(void visitor(DartdocOption)) {
_children.values.forEach((d) => d.traverse(visitor));
}
}
/// A [DartdocOption] that only exists as a command line argument. --help would
/// be a good example.
class DartdocOptionArgOnly<T> extends DartdocOption<T>
with _DartdocArgOption<T> {
String _abbr;
bool _hide;
bool _negatable;
bool _splitCommas;
DartdocOptionArgOnly(String name, T defaultsTo,
{String abbr,
bool mustExist = false,
String help = '',
bool hide = false,
bool isDir = false,
bool isFile = false,
bool negatable = false,
bool splitCommas})
: super._(name, defaultsTo, help, isDir, isFile, mustExist, null) {
_hide = hide;
_negatable = negatable;
_splitCommas = splitCommas;
_abbr = abbr;
}
@override
String get abbr => _abbr;
@override
bool get hide => _hide;
@override
bool get negatable => _negatable;
@override
bool get splitCommas => _splitCommas;
}
/// A [DartdocOption] that works with command line arguments and dartdoc_options files.
class DartdocOptionArgFile<T> extends DartdocOption<T>
with _DartdocArgOption<T>, _DartdocFileOption<T> {
String _abbr;
bool _hide;
bool _negatable;
bool _parentDirOverridesChild;
bool _splitCommas;
DartdocOptionArgFile(String name, T defaultsTo,
{String abbr,
bool mustExist = false,
String help: '',
bool hide = false,
bool isDir = false,
bool isFile = false,
bool negatable = false,
bool parentDirOverridesChild: false,
bool splitCommas})
: super._(name, defaultsTo, help, isDir, isFile, mustExist, null) {
_abbr = abbr;
_hide = hide;
_negatable = negatable;
_parentDirOverridesChild = parentDirOverridesChild;
_splitCommas = splitCommas;
}
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingPath) {
if (valueWithContext.definingFile != null) {
_onMissingFromFiles(valueWithContext, missingPath);
} else {
_onMissingFromArgs(valueWithContext, missingPath);
}
}
/// Try to find an explicit argument setting this value, but if not, fall back to files
/// finally, the default.
@override
T valueAt(Directory dir) {
T value = _valueAtFromArgs();
if (value == null) value = _valueAtFromFiles(dir);
if (value == null) value = defaultsTo;
return value;
}
@override
String get abbr => _abbr;
@override
bool get hide => _hide;
@override
bool get negatable => _negatable;
@override
bool get parentDirOverridesChild => _parentDirOverridesChild;
@override
bool get splitCommas => _splitCommas;
}
class DartdocOptionFileOnly<T> extends DartdocOption<T>
with _DartdocFileOption<T> {
bool _parentDirOverridesChild;
DartdocOptionFileOnly(String name, T defaultsTo,
{bool mustExist = false,
String help: '',
bool isDir = false,
bool isFile = false,
bool parentDirOverridesChild: false,
T Function(YamlMap, pathLib.Context) convertYamlToType})
: super._(name, defaultsTo, help, isDir, isFile, mustExist,
convertYamlToType) {
_parentDirOverridesChild = parentDirOverridesChild;
}
@override
bool get parentDirOverridesChild => _parentDirOverridesChild;
}
/// Implements checking for options contained in dartdoc.yaml.
abstract class _DartdocFileOption<T> implements DartdocOption<T> {
/// If true, the parent directory's value overrides the child's. Otherwise, the child's
/// value overrides values in parents.
bool get parentDirOverridesChild;
/// The name of the option, with nested options joined by [.]. For example:
///
/// ```yaml
/// dartdoc:
/// stuff:
/// things:
/// ```
/// would have the name `things` and the fieldName `dartdoc.stuff.things`.
String get fieldName => keys.join('.');
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingPath) =>
_onMissingFromFiles(valueWithContext, missingPath);
void _onMissingFromFiles(
_OptionValueWithContext valueWithContext, String missingPath) {
String dartdocYaml = pathLib.join(
valueWithContext.canonicalDirectoryPath, valueWithContext.definingFile);
throw new DartdocFileMissing(
'Field ${fieldName} from ${dartdocYaml}, set to ${valueWithContext.value}, resolves to missing path: "${missingPath}"');
}
@override
/// Searches for a value in configuration files relative to [dir], and if not
/// found, returns [defaultsTo].
T valueAt(Directory dir) {
return _valueAtFromFiles(dir) ?? defaultsTo;
}
final Map<String, T> __valueAtFromFiles = new Map();
// The value of this option from files will not change unless files are
// modified during execution (not allowed in Dartdoc).
T _valueAtFromFiles(Directory dir) {
String key = pathLib.canonicalize(dir.path);
if (!__valueAtFromFiles.containsKey(key)) {
_OptionValueWithContext valueWithContext;
if (parentDirOverridesChild) {
valueWithContext = _valueAtFromFilesLastFound(dir);
} else {
valueWithContext = _valueAtFromFilesFirstFound(dir);
}
__valueAtFromFiles[key] = _handlePathsInContext(valueWithContext);
}
return __valueAtFromFiles[key];
}
/// Searches all dartdoc_options files through parent directories,
/// starting at [dir], for the option and returns one once
/// found.
_OptionValueWithContext _valueAtFromFilesFirstFound(Directory dir) {
_OptionValueWithContext value;
while (true) {
value = _valueAtFromFile(dir);
if (value != null || pathLib.equals(dir.parent.path, dir.path)) break;
dir = dir.parent;
}
return value;
}
/// Searches all dartdoc_options files for the option, and returns the
/// value in the top-most parent directory dartdoc_options.yaml file it is
/// mentioned in.
_OptionValueWithContext _valueAtFromFilesLastFound(Directory dir) {
_OptionValueWithContext value;
while (true) {
_OptionValueWithContext tmpValue = _valueAtFromFile(dir);
if (tmpValue != null) value = tmpValue;
if (pathLib.equals(dir.parent.path, dir.path)) break;
dir = dir.parent;
}
return value;
}
/// Returns null if not set in the yaml file in this directory (or its
/// parents).
_OptionValueWithContext _valueAtFromFile(Directory dir) {
_YamlFileData yamlFileData = _yamlAtDirectory(dir);
String contextPath = yamlFileData.canonicalDirectoryPath;
dynamic yamlData = yamlFileData.data;
for (String key in keys) {
if (!yamlData.containsKey(key)) return null;
yamlData = yamlData[key];
}
var returnData;
if (_isListString) {
if (yamlData is YamlList) {
returnData = <String>[];
for (var item in yamlData) {
returnData.add(item.toString());
}
}
} else if (yamlData is YamlMap) {
// TODO(jcollins-g): This special casing is unfortunate. Consider
// a refactor to extract yaml data conversion into closures 100% of the
// time or find a cleaner way to do this.
//
// A refactor probably would integrate resolvedValue for
// _OptionValueWithContext into the return data here, and would not have
// that be separate.
if (_isMapString && _convertYamlToType == null) {
_convertYamlToType = (YamlMap yamlMap, pathLib.Context pathContext) {
var returnData = <String, String>{};
for (MapEntry entry in yamlMap.entries) {
returnData[entry.key.toString()] = entry.value.toString();
}
return returnData as T;
};
}
if (_convertYamlToType == null) {
throw new DartdocOptionError(
'Unable to convert yaml to type for option: $fieldName, method not defined');
}
String canonicalDirectoryPath = pathLib.canonicalize(contextPath);
returnData = _convertYamlToType(
yamlData, new pathLib.Context(current: canonicalDirectoryPath));
} else if (_isDouble) {
if (yamlData is num) {
returnData = yamlData.toDouble();
}
} else if (_isInt || _isString || _isBool) {
if (yamlData is T) {
returnData = yamlData;
}
} else {
throw new UnsupportedError('Type ${T} is not supported');
}
return new _OptionValueWithContext(returnData as T, contextPath,
definingFile: 'dartdoc_options.yaml');
}
_YamlFileData _yamlAtDirectory(Directory dir) {
List<String> canonicalPaths = [pathLib.canonicalize(dir.path)];
if (!_yamlAtCanonicalPathCache.containsKey(canonicalPaths.first)) {
_YamlFileData yamlData =
new _YamlFileData(new Map(), directoryCurrentPath);
if (dir.existsSync()) {
File dartdocOptionsFile;
while (true) {
dartdocOptionsFile =
new File(pathLib.join(dir.path, 'dartdoc_options.yaml'));
if (dartdocOptionsFile.existsSync() ||
pathLib.equals(dir.parent.path, dir.path)) break;
dir = dir.parent;
canonicalPaths.add(pathLib.canonicalize(dir.path));
}
if (dartdocOptionsFile.existsSync()) {
yamlData = new _YamlFileData(
loadYaml(dartdocOptionsFile.readAsStringSync()),
pathLib.canonicalize(dir.path));
}
}
canonicalPaths.forEach((p) => _yamlAtCanonicalPathCache[p] = yamlData);
}
return _yamlAtCanonicalPathCache[canonicalPaths.first];
}
}
/// Mixin class implementing command-line arguments for [DartdocOption].
abstract class _DartdocArgOption<T> implements DartdocOption<T> {
/// For [ArgParser], set to true if the argument can be negated with --no on the command line.
bool get negatable;
/// For [ArgParser], set to true if a single string argument will be broken into a list on commas.
bool get splitCommas;
/// For [ArgParser], set to true to hide this from the help menu.
bool get hide;
/// For [ArgParser], set to a single character to have a short version of the command line argument.
String get abbr;
/// valueAt for arguments ignores the [dir] parameter and only uses command
/// line arguments and the current working directory to resolve the result.
@override
T valueAt(Directory dir) => _valueAtFromArgs() ?? defaultsTo;
/// For passing in to [int.parse] and [double.parse] `onError'.
void _throwErrorForTypes(String value) {
String example;
if (defaultsTo is Map) {
example = 'key::value';
} else if (_isInt) {
example = '32';
} else if (_isDouble) {
example = '0.76';
}
throw new DartdocOptionError(
'Invalid argument value: --${argName}, set to "${value}", must be a ${T}. Example: --${argName} ${example}');
}
/// Returns null if no argument was given on the command line.
T _valueAtFromArgs() {
_OptionValueWithContext valueWithContext = _valueAtFromArgsWithContext();
return _handlePathsInContext(valueWithContext);
}
@override
void _onMissing(
_OptionValueWithContext valueWithContext, String missingPath) =>
_onMissingFromArgs(valueWithContext, missingPath);
void _onMissingFromArgs(
_OptionValueWithContext valueWithContext, String missingPath) {
throw new DartdocFileMissing(
'Argument --${argName}, set to ${valueWithContext.value}, resolves to missing path: "${missingPath}"');
}
/// Generates an _OptionValueWithContext using the value of the argument from
/// the [argParser] and the working directory from [directoryCurrent].
///
/// Throws [UnsupportedError] if [T] is not a supported type.
_OptionValueWithContext _valueAtFromArgsWithContext() {
if (!_argResults.wasParsed(argName)) return null;
T retval;
// Unlike in _DartdocFileOption, we throw here on inputs being invalid rather
// than silently proceeding. TODO(jcollins-g): throw on input formatting for
// files too?
if (_isBool || _isListString || _isString) {
retval = _argResults[argName];
} else if (_isInt) {
retval = int.tryParse(_argResults[argName]) as T;
if (retval == null) _throwErrorForTypes(_argResults[argName]);
} else if (_isDouble) {
retval = double.tryParse(_argResults[argName]) as T;
if (retval == null) _throwErrorForTypes(_argResults[argName]);
} else if (_isMapString) {
retval = <String, String>{} as T;
for (String pair in _argResults[argName]) {
List<String> pairList = pair.split('::');
if (pairList.length != 2) {
_throwErrorForTypes(pair);
}
assert(pairList.length == 2);
(retval as Map<String, String>)[pairList.first] = pairList.last;
}
} else {
throw UnsupportedError('Type ${T} is not supported');
}
return new _OptionValueWithContext(retval, directoryCurrentPath);
}
/// The name of this option as a command line argument.
String get argName => _keysToArgName(keys);
/// Turns ['foo', 'somethingBar', 'over_the_hill'] into
/// 'something-bar-over-the-hill' (with default skip).
/// This allows argument names to reflect nested structure.
static String _keysToArgName(Iterable<String> keys, [int skip = 1]) {
String argName = "${keys.skip(skip).join('-')}";
argName = argName.replaceAll('_', '-');
// Do not consume the lowercase character after the uppercase one, to handle
// two character words.
final camelCaseRegexp = new RegExp(r'([a-z])([A-Z])(?=([a-z]))');
argName = argName.replaceAllMapped(camelCaseRegexp, (Match m) {
String before = m.group(1);
String after = m.group(2).toLowerCase();
return '${before}-${after}';
});
return argName;
}
/// If this argument is added to a larger tree of DartdocOptions, call
/// [ArgParser.addFlag], [ArgParser.addOption], or [ArgParser.addMultiOption]
/// as appropriate for [T].
@override
void _onAdd() {
if (_isBool) {
argParser.addFlag(argName,
abbr: abbr,
defaultsTo: defaultsTo as bool,
help: help,
hide: hide,
negatable: negatable ?? false);
} else if (_isInt || _isDouble || _isString) {
argParser.addOption(argName,
abbr: abbr,
defaultsTo: defaultsTo?.toString() ?? null,
help: help,
hide: hide);
} else if (_isListString || _isMapString) {
List<String> defaultsToList = [];
if (_isListString) {
defaultsToList = defaultsTo as List<String>;
} else {
defaultsToList.addAll((defaultsTo as Map<String, String>)
.entries
.map((m) => '${m.key}::${m.value}'));
}
argParser.addMultiOption(argName,
abbr: abbr,
defaultsTo: defaultsToList,
help: help,
hide: hide,
splitCommas: splitCommas);
} else {
throw new UnsupportedError('Type ${T} is not supported');
}
}
}
/// An [DartdocOptionSet] wrapped in nice accessors specific to Dartdoc, which
/// automatically passes in the right directory for a given context. Usually,
/// a single [ModelElement], [Package], [Category] and so forth has a single context
/// and so this can be made a member variable of those structures.
class DartdocOptionContext {
final DartdocOptionSet optionSet;
Directory context;
// TODO(jcollins-g): Allow passing in structured data to initialize a
// [DartdocOptionContext]'s arguments instead of having to parse strings
// via optionSet.
/// If [entity] is null, assume this is the initialization case and use
/// the inputDir flag to determine the context.
DartdocOptionContext(this.optionSet, FileSystemEntity entity) {
if (entity == null) {
String inputDir = optionSet['inputDir'].valueAt(directoryCurrent) ??
directoryCurrentPath;
context = new Directory(inputDir);
} else {
context = new Directory(pathLib
.canonicalize(entity is File ? entity.parent.path : entity.path));
}
}
/// Build a DartdocOptionContext from an analyzer element (using its source
/// location).
factory DartdocOptionContext.fromElement(
DartdocOptionSet optionSet, Element element) {
return new DartdocOptionContext(
optionSet, new File(element.source.fullName));
}
/// Build a DartdocOptionContext from an existing [DartdocOptionContext] and a new analyzer [Element].
factory DartdocOptionContext.fromContextElement(
DartdocOptionContext optionContext, Element element) {
return new DartdocOptionContext.fromElement(
optionContext.optionSet, element);
}
/// Build a DartdocOptionContext from an existing [DartdocOptionContext].
factory DartdocOptionContext.fromContext(
DartdocOptionContext optionContext, FileSystemEntity entity) {
return new DartdocOptionContext(optionContext.optionSet, entity);
}
// All values defined in createDartdocOptions should be exposed here.
bool get allowTools => optionSet['allowTools'].valueAt(context);
double get ambiguousReexportScorerMinConfidence =>
optionSet['ambiguousReexportScorerMinConfidence'].valueAt(context);
bool get autoIncludeDependencies =>
optionSet['autoIncludeDependencies'].valueAt(context);
List<String> get categoryOrder => optionSet['categoryOrder'].valueAt(context);
CategoryConfiguration get categories =>
optionSet['categories'].valueAt(context);
List<String> get dropTextFrom => optionSet['dropTextFrom'].valueAt(context);
String get examplePathPrefix =>
optionSet['examplePathPrefix'].valueAt(context);
List<String> get exclude => optionSet['exclude'].valueAt(context);
List<String> get excludePackages =>
optionSet['excludePackages'].valueAt(context);
String get flutterRoot => optionSet['flutterRoot'].valueAt(context);
bool get hideSdkText => optionSet['hideSdkText'].valueAt(context);
List<String> get include => optionSet['include'].valueAt(context);
List<String> get includeExternal =>
optionSet['includeExternal'].valueAt(context);
bool get includeSource => optionSet['includeSource'].valueAt(context);
bool get injectHtml => optionSet['injectHtml'].valueAt(context);
ToolConfiguration get tools => optionSet['tools'].valueAt(context);
/// _input is only used to construct synthetic options.
// ignore: unused_element
String get _input => optionSet['input'].valueAt(context);
String get inputDir => optionSet['inputDir'].valueAt(context);
bool get linkToRemote => optionSet['linkTo']['remote'].valueAt(context);
String get linkToUrl => optionSet['linkTo']['url'].valueAt(context);
/// _linkToHosted is only used to construct synthetic options.
// ignore: unused_element
String get _linkToHosted => optionSet['linkTo']['hosted'].valueAt(context);
String get output => optionSet['output'].valueAt(context);
PackageMeta get packageMeta => optionSet['packageMeta'].valueAt(context);
List<String> get packageOrder => optionSet['packageOrder'].valueAt(context);
bool get sdkDocs => optionSet['sdkDocs'].valueAt(context);
String get sdkDir => optionSet['sdkDir'].valueAt(context);
bool get showUndocumentedCategories =>
optionSet['showUndocumentedCategories'].valueAt(context);
bool get showWarnings => optionSet['showWarnings'].valueAt(context);
PackageMeta get topLevelPackageMeta =>
optionSet['topLevelPackageMeta'].valueAt(context);
bool get useCategories => optionSet['useCategories'].valueAt(context);
bool get validateLinks => optionSet['validateLinks'].valueAt(context);
bool get verboseWarnings => optionSet['verboseWarnings'].valueAt(context);
bool isLibraryExcluded(String name) =>
exclude.any((pattern) => name == pattern);
bool isPackageExcluded(String name) =>
excludePackages.any((pattern) => name == pattern);
}
/// Instantiate dartdoc's configuration file and options parser with the
/// given command line arguments.
Future<List<DartdocOption>> createDartdocOptions() async {
return <DartdocOption>[
new DartdocOptionArgOnly<bool>('allowTools', true,
help: 'Execute user-defined tools to fill in @tool directives.',
negatable: true),
new DartdocOptionArgFile<double>(
'ambiguousReexportScorerMinConfidence', 0.1,
help: 'Minimum scorer confidence to suppress warning on ambiguous '
'reexport.'),
new DartdocOptionArgOnly<bool>('autoIncludeDependencies', false,
help: 'Include all the used libraries into the docs, even the ones not '
'in the current package or "include-external"',
negatable: true),
new DartdocOptionArgFile<List<String>>('categoryOrder', [],
help:
"A list of categories (not package names) to place first when grouping symbols on dartdoc's sidebar. "
'Unmentioned categories are sorted after these.'),
new DartdocOptionFileOnly<CategoryConfiguration>(
'categories', CategoryConfiguration.empty,
convertYamlToType: CategoryConfiguration.fromYamlMap,
help: 'A list of all categories, their display names, and markdown '
'documentation in the order they are to be displayed.'),
new DartdocOptionSyntheticOnly<List<String>>('dropTextFrom',
(DartdocSyntheticOption<List<String>> option, Directory dir) {
if (option.parent['hideSdkText'].valueAt(dir)) {
return [
'dart.async',
'dart.collection',
'dart.convert',
'dart.core',
'dart.developer',
'dart.html',
'dart.indexed_db',
'dart.io',
'dart.lisolate',
'dart.js',
'dart.js_util',
'dart.math',
'dart.mirrors',
'dart.svg',
'dart.typed_data',
'dart.web_audio'
];
}
return [];
}, help: 'Remove text from libraries with the following names.'),
new DartdocOptionArgFile<String>('examplePathPrefix', null,
isDir: true,
help: 'Prefix for @example paths.\n(defaults to the project root)',
mustExist: true),
new DartdocOptionArgFile<List<String>>('exclude', [],
help: 'Library names to ignore.', splitCommas: true),
new DartdocOptionArgOnly<List<String>>('excludePackages', [],
help: 'Package names to ignore.', splitCommas: true),
// This could be a ArgOnly, but trying to not provide too many ways
// to set the flutter root.
new DartdocOptionSyntheticOnly<String>(
'flutterRoot',
(DartdocSyntheticOption<String> option, Directory dir) =>
resolveTildePath(Platform.environment['FLUTTER_ROOT']),
isDir: true,
help: 'Root of the Flutter SDK, specified from environment.',
mustExist: true),
new DartdocOptionArgOnly<bool>('hideSdkText', false,
hide: true,
help: 'Drop all text for SDK components. Helpful for integration '
'tests for dartdoc, probably not useful for anything else.',
negatable: true),
new DartdocOptionArgFile<List<String>>('include', [],
help: 'Library names to generate docs for.', splitCommas: true),
new DartdocOptionArgFile<List<String>>('includeExternal', null,
isFile: true,
help:
'Additional (external) dart files to include; use "dir/fileName", '
'as in lib/material.dart.',
mustExist: true,
splitCommas: true),
new DartdocOptionArgOnly<bool>('includeSource', true,
help: 'Show source code blocks.', negatable: true),
new DartdocOptionArgOnly<bool>('injectHtml', false,
help: 'Allow the use of the {@inject-html} directive to inject raw '
'HTML into dartdoc output.'),
new DartdocOptionArgOnly<String>('input', directoryCurrentPath,
isDir: true, help: 'Path to source directory', mustExist: true),
new DartdocOptionSyntheticOnly<String>('inputDir',
(DartdocSyntheticOption<String> option, Directory dir) {
if (option.parent['sdkDocs'].valueAt(dir)) {
return option.parent['sdkDir'].valueAt(dir);
}
return option.parent['input'].valueAt(dir);
},
help: 'Path to source directory (with override if --sdk-docs)',
isDir: true,
mustExist: true),
new DartdocOptionSet('linkTo')
..addAll([
new DartdocOptionArgOnly<Map<String, String>>(
'hosted',
{
'pub.dartlang.org':
'https://pub.dartlang.org/documentation/%n%/%v%'
},
help: 'Specify URLs for hosted pub packages'),
new DartdocOptionArgOnly<Map<String, String>>(
'sdks',
{
'Dart': 'https://api.dartlang.org/%b%/%v%',
'Flutter': 'https://docs.flutter.io/flutter',
},
help: 'Specify URLs for SDKs.',
),
new DartdocOptionFileSynth<String>('url',
(DartdocSyntheticOption<String> option, Directory dir) {
PackageMeta packageMeta =
option.parent.parent['packageMeta'].valueAt(dir);
// Prefer SDK check first, then pub cache check.
String inSdk = packageMeta
.sdkType(option.parent.parent['flutterRoot'].valueAt(dir));
if (inSdk != null) {
Map<String, String> sdks = option.parent['sdks'].valueAt(dir);
if (sdks.containsKey(inSdk)) return sdks[inSdk];
}
String hostedAt = packageMeta.hostedAt;
if (hostedAt != null) {
Map<String, String> hostMap = option.parent['hosted'].valueAt(dir);
if (hostMap.containsKey(hostedAt)) return hostMap[hostedAt];
}
return '';
}, help: 'Url to use for this particular package.'),
new DartdocOptionArgOnly<bool>('remote', false,
help: 'Allow links to be generated for packages outside this one.',
negatable: true),
]),
new DartdocOptionArgOnly<String>('output', pathLib.join('doc', 'api'),
isDir: true, help: 'Path to output directory.'),
new DartdocOptionSyntheticOnly<PackageMeta>(
'packageMeta',
(DartdocSyntheticOption<PackageMeta> option, Directory dir) {
PackageMeta packageMeta = new PackageMeta.fromDir(dir);
if (packageMeta == null) {
throw new DartdocOptionError(
'Unable to determine package for directory: ${dir.path}');
}
return packageMeta;
},
),
new DartdocOptionArgOnly<List<String>>('packageOrder', [],
help:
'A list of package names to place first when grouping libraries in packages. '
'Unmentioned packages are sorted after these.'),
new DartdocOptionArgOnly<bool>('sdkDocs', false,
help: 'Generate ONLY the docs for the Dart SDK.'),
new DartdocOptionArgSynth<String>('sdkDir',
(DartdocSyntheticOption<String> option, Directory dir) {
if (!option.parent['sdkDocs'].valueAt(dir) &&
(option.root['topLevelPackageMeta'].valueAt(dir) as PackageMeta)
.requiresFlutter) {
String flutterRoot = option.root['flutterRoot'].valueAt(dir);
if (flutterRoot == null) {
throw new DartdocOptionError(
'Top level package requires Flutter but FLUTTER_ROOT environment variable not set');
}
return pathLib.join(flutterRoot, 'bin', 'cache', 'dart-sdk');
}
return defaultSdkDir.absolute.path;
}, help: 'Path to the SDK directory.', isDir: true, mustExist: true),
new DartdocOptionArgFile<bool>('showUndocumentedCategories', false,
help: "Label categories that aren't documented", negatable: true),
new DartdocOptionArgOnly<bool>('showWarnings', false,
help: 'Display all warnings.'),
new DartdocOptionSyntheticOnly<PackageMeta>('topLevelPackageMeta',
(DartdocSyntheticOption<PackageMeta> option, Directory dir) {
PackageMeta packageMeta = new PackageMeta.fromDir(
new Directory(option.parent['inputDir'].valueAt(dir)));
if (packageMeta == null) {
throw new DartdocOptionError(
'Unable to generate documentation: no package found');
}
if (!packageMeta.isValid) {
final String firstError = packageMeta.getInvalidReasons().first;
throw new DartdocOptionError('Package is invalid: $firstError');
}
return packageMeta;
}, help: 'PackageMeta object for the default package.'),
new DartdocOptionArgOnly<bool>('useCategories', true,
help: 'Display categories in the sidebar of packages'),
new DartdocOptionArgOnly<bool>('validateLinks', true,
help:
'Runs the built-in link checker to display Dart context aware warnings for broken links (slow)',
negatable: true),
new DartdocOptionArgOnly<bool>('verboseWarnings', true,
help: 'Display extra debugging information and help with warnings.',
negatable: true),
new DartdocOptionFileOnly<ToolConfiguration>(
'tools', ToolConfiguration.empty,
convertYamlToType: ToolConfiguration.fromYamlMap,
help: 'A map of tool names to executable paths. Each executable must '
'exist. Executables for different platforms are specified by '
'giving the platform name as a key, and a list of strings as the '
'command.'),
];
}