Skip to content

Commit de381e6

Browse files
authored
Refactor DartdocConfig object usage, part 3 (#1673)
1 parent fe8a95f commit de381e6

File tree

3 files changed

+305
-7
lines changed

3 files changed

+305
-7
lines changed

lib/src/dartdoc_options.dart

Lines changed: 260 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ library dartdoc.dartdoc_options;
1616

1717
import 'dart:io';
1818

19+
import 'package:analyzer/dart/element/element.dart';
1920
import 'package:args/args.dart';
2021
import 'package:dartdoc/dartdoc.dart';
2122
import 'package:path/path.dart' as pathLib;
@@ -32,6 +33,22 @@ const int _kIntVal = 0;
3233
const double _kDoubleVal = 0.0;
3334
const bool _kBoolVal = true;
3435

36+
String _resolveTildePath(String originalPath) {
37+
if (originalPath == null || !originalPath.startsWith('~/')) {
38+
return originalPath;
39+
}
40+
41+
String homeDir;
42+
43+
if (Platform.isWindows) {
44+
homeDir = pathLib.absolute(Platform.environment['USERPROFILE']);
45+
} else {
46+
homeDir = pathLib.absolute(Platform.environment['HOME']);
47+
}
48+
49+
return pathLib.join(homeDir, originalPath.substring(2));
50+
}
51+
3552
class DartdocOptionError extends DartdocFailure {
3653
DartdocOptionError(String details) : super(details);
3754
}
@@ -80,10 +97,12 @@ class _OptionValueWithContext<T> {
8097
/// if [T] isn't a [String] or [List<String>].
8198
T get resolvedValue {
8299
if (value is List<String>) {
83-
return (value as List<String>).map((v) => pathContext.canonicalize(v))
84-
as T;
100+
return (value as List<String>)
101+
.map((v) => pathContext.canonicalize(_resolveTildePath(v)))
102+
.cast<String>()
103+
.toList() as T;
85104
} else if (value is String) {
86-
return pathContext.canonicalize(value as String) as T;
105+
return pathContext.canonicalize(_resolveTildePath(value as String)) as T;
87106
} else {
88107
throw new UnsupportedError('Type $T is not supported for resolvedValue');
89108
}
@@ -249,6 +268,13 @@ abstract class DartdocOption<T> {
249268
/// corresponding files or directories.
250269
T valueAt(Directory dir);
251270

271+
/// Calls [valueAt] with the current working directory.
272+
T valueAtCurrent() => valueAt(Directory.current);
273+
274+
/// Calls [valueAt] on the directory this element is defined in.
275+
T valueAtElement(Element element) => valueAt(new Directory(
276+
pathLib.canonicalize(pathLib.basename(element.source.fullName))));
277+
252278
/// Adds a DartdocOption to the children of this DartdocOption.
253279
void add(DartdocOption option) {
254280
if (_children.containsKey(option.name))
@@ -278,6 +304,38 @@ abstract class DartdocOption<T> {
278304
}
279305
}
280306

307+
/// A synthetic option takes a closure at construction time that computes
308+
/// the value of the configuration option based on other configuration options.
309+
/// Does not protect against closures that self-reference. If [mustExist] and
310+
/// [isDir] or [isFile] is set, computed values will be resolved to canonical
311+
/// paths.
312+
class DartdocOptionSynthetic<T> extends DartdocOption<T> {
313+
T Function(DartdocOptionSynthetic, Directory) _compute;
314+
315+
DartdocOptionSynthetic(String name, this._compute,
316+
{bool mustExist = false,
317+
String help = '',
318+
bool isDir = false,
319+
bool isFile = false})
320+
: super._(name, null, help, isDir, isFile, mustExist);
321+
322+
@override
323+
T valueAt(Directory dir) {
324+
_OptionValueWithContext context =
325+
new _OptionValueWithContext<T>(_compute(this, dir), dir.path);
326+
return _handlePathsInContext(context);
327+
}
328+
329+
@override
330+
void _onMissing(
331+
_OptionValueWithContext valueWithContext, String missingPath) {
332+
String description =
333+
'Synthetic configuration option ${name} from <internal>';
334+
throw new DartdocFileMissing(
335+
'$description, computed as ${valueWithContext.value}, resolves to missing path: "${missingPath}"');
336+
}
337+
}
338+
281339
/// A [DartdocOption] that only contains other [DartdocOption]s and is not an option itself.
282340
class DartdocOptionSet extends DartdocOption<Null> {
283341
DartdocOptionSet(String name)
@@ -292,9 +350,8 @@ class DartdocOptionSet extends DartdocOption<Null> {
292350
void _onMissing(
293351
_OptionValueWithContext valueWithContext, String missingFilename) {}
294352

295-
@override
296-
297353
/// Traverse skips this node, because it doesn't represent a real configuration object.
354+
@override
298355
void traverse(void visitor(DartdocOption)) {
299356
_children.values.forEach((d) => d.traverse(visitor));
300357
}
@@ -312,7 +369,7 @@ class DartdocOptionArgOnly<T> extends DartdocOption<T>
312369
DartdocOptionArgOnly(String name, T defaultsTo,
313370
{String abbr,
314371
bool mustExist = false,
315-
help: '',
372+
String help = '',
316373
bool hide,
317374
bool isDir = false,
318375
bool isFile = false,
@@ -708,6 +765,203 @@ abstract class _DartdocArgOption<T> implements DartdocOption<T> {
708765
}
709766
}
710767

768+
/// An [DartdocOptionSet] wrapped in nice accessors specific to Dartdoc, which
769+
/// automatically passes in the right directory for a given context. Usually,
770+
/// a single [ModelElement], [Package], [Category] and so forth has a single context
771+
/// and so this can be made a member variable of those structures.
772+
class DartdocOptionContext {
773+
final DartdocOptionSet optionSet;
774+
Directory _context;
775+
776+
DartdocOptionContext(this.optionSet, FileSystemEntity entity) {
777+
_context = new Directory(pathLib
778+
.canonicalize(entity is File ? entity.parent.path : entity.path));
779+
}
780+
781+
/// Build a [DartdocOptionSet] and associate it with a [DartdocOptionContext]
782+
/// based on the input directory.
783+
factory DartdocOptionContext.fromArgv(List<String> argv) {
784+
DartdocOptionSet optionSet = _createDartdocOptions(argv);
785+
String inputDir = optionSet['input'].valueAtCurrent();
786+
return new DartdocOptionContext(optionSet, new Directory(inputDir));
787+
}
788+
789+
/// Build a DartdocOptionContext from an analyzer element (using its source
790+
/// location).
791+
factory DartdocOptionContext.fromElement(
792+
DartdocOptionSet optionSet, Element element) {
793+
return new DartdocOptionContext(
794+
optionSet, new File(element.source.fullName));
795+
}
796+
797+
/// Build a DartdocOptionContext from an existing [DartdocOptionContext] and a new analyzer [Element].
798+
factory DartdocOptionContext.fromContextElement(
799+
DartdocOptionContext optionContext, Element element) {
800+
return new DartdocOptionContext.fromElement(
801+
optionContext.optionSet, element);
802+
}
803+
804+
/// Build a DartdocOptionContext from an existing [DartdocOptionContext].
805+
factory DartdocOptionContext.fromContext(
806+
DartdocOptionContext optionContext, FileSystemEntity entity) {
807+
return new DartdocOptionContext(optionContext.optionSet, entity);
808+
}
809+
810+
// All values defined in createDartdocOptions should be exposed here.
811+
812+
bool get addCrossdart => optionSet['addCrossdart'].valueAt(_context);
813+
double get ambiguousReexportScorerMinConfidence =>
814+
optionSet['ambiguousReexportScorerMinConfidence'].valueAt(_context);
815+
bool get autoIncludeDependencies =>
816+
optionSet['autoIncludeDependencies'].valueAt(_context);
817+
List<String> get categoryOrder =>
818+
optionSet['categoryOrder'].valueAt(_context);
819+
String get examplePathPrefix =>
820+
optionSet['examplePathPrefix'].valueAt(_context);
821+
List<String> get exclude => optionSet['exclude'].valueAt(_context);
822+
List<String> get excludePackages =>
823+
optionSet['excludePackages'].valueAt(_context);
824+
String get favicon => optionSet['favicon'].valueAt(_context);
825+
List<String> get footer => optionSet['footer'].valueAt(_context);
826+
List<String> get footerText => optionSet['footerText'].valueAt(_context);
827+
List<String> get header => optionSet['header'].valueAt(_context);
828+
bool get help => optionSet['help'].valueAt(_context);
829+
bool get hideSdkText => optionSet['hideSdkText'].valueAt(_context);
830+
String get hostedUrl => optionSet['hostedUrl'].valueAt(_context);
831+
List<String> get include => optionSet['include'].valueAt(_context);
832+
List<String> get includeExternal =>
833+
optionSet['includeExternal'].valueAt(_context);
834+
bool get includeSource => optionSet['includeSource'].valueAt(_context);
835+
String get input => optionSet['input'].valueAt(_context);
836+
bool get json => optionSet['json'].valueAt(_context);
837+
String get output => optionSet['output'].valueAt(_context);
838+
List<String> get packageOrder => optionSet['packageOrder'].valueAt(_context);
839+
bool get prettyIndexJson => optionSet['prettyIndexJson'].valueAt(_context);
840+
String get relCanonicalPrefix =>
841+
optionSet['relCanonicalPrefix'].valueAt(_context);
842+
bool get sdkDocs => optionSet['sdkDocs'].valueAt(_context);
843+
String get sdkDir => optionSet['sdkDir'].valueAt(_context);
844+
bool get showProgress => optionSet['showProgress'].valueAt(_context);
845+
bool get showWarnings => optionSet['showWarnings'].valueAt(_context);
846+
bool get useCategories => optionSet['useCategories'].valueAt(_context);
847+
bool get validateLinks => optionSet['validateLinks'].valueAt(_context);
848+
bool get verboseWarnings => optionSet['verboseWarnings'].valueAt(_context);
849+
bool get version => optionSet['version'].valueAt(_context);
850+
}
851+
852+
/// Instantiate dartdoc's configuration file and options parser with the
853+
/// given command line arguments.
854+
DartdocOptionSet _createDartdocOptions(List<String> argv) {
855+
// Sync with DartdocOptionContext.
856+
return new DartdocOptionSet('dartdoc')
857+
..addAll([
858+
new DartdocOptionArgOnly<bool>('addCrossdart', false,
859+
help: 'Add Crossdart links to the source code pieces.',
860+
negatable: false),
861+
new DartdocOptionBoth<double>('ambiguousReexportScorerMinConfidence', 0.1,
862+
help:
863+
'Minimum scorer confidence to suppress warning on ambiguous reexport.'),
864+
new DartdocOptionArgOnly<bool>('autoIncludeDependencies', false,
865+
help:
866+
'Include all the used libraries into the docs, even the ones not in the current package or "include-external"',
867+
negatable: false),
868+
new DartdocOptionBoth<List<String>>('categoryOrder', [],
869+
help:
870+
"A list of categories (not package names) to place first when grouping symbols on dartdoc's sidebar. "
871+
'Unmentioned categories are sorted after these.'),
872+
new DartdocOptionBoth<String>('examplePathPrefix', null,
873+
isDir: true,
874+
help: 'Prefix for @example paths.\n(defaults to the project root)',
875+
mustExist: true),
876+
new DartdocOptionBoth<List<String>>('exclude', [],
877+
help: 'Library names to ignore.', splitCommas: true),
878+
new DartdocOptionBoth<List<String>>('excludePackages', [],
879+
help: 'Package names to ignore.', splitCommas: true),
880+
new DartdocOptionBoth<String>('favicon', null,
881+
isFile: true,
882+
help: 'A path to a favicon for the generated docs.',
883+
mustExist: true),
884+
new DartdocOptionBoth<List<String>>('footer', [],
885+
isFile: true,
886+
help: 'paths to footer files containing HTML text.',
887+
mustExist: true,
888+
splitCommas: true),
889+
new DartdocOptionBoth<List<String>>('footerText', [],
890+
isFile: true,
891+
help:
892+
'paths to footer-text files (optional text next to the package name '
893+
'and version).',
894+
mustExist: true,
895+
splitCommas: true),
896+
new DartdocOptionBoth<List<String>>('header', [],
897+
isFile: true,
898+
help: 'paths to header files containing HTML text.',
899+
splitCommas: true),
900+
new DartdocOptionArgOnly<bool>('help', false,
901+
abbr: 'h', help: 'Show command help.', negatable: false),
902+
new DartdocOptionArgOnly<bool>('hideSdkText', false,
903+
hide: true,
904+
help:
905+
'Drop all text for SDK components. Helpful for integration tests for dartdoc, probably not useful for anything else.',
906+
negatable: true),
907+
new DartdocOptionArgOnly<String>('hostedUrl', null,
908+
help:
909+
'URL where the docs will be hosted (used to generate the sitemap).'),
910+
new DartdocOptionBoth<List<String>>('include', null,
911+
help: 'Library names to generate docs for.', splitCommas: true),
912+
new DartdocOptionBoth<List<String>>('includeExternal', null,
913+
isFile: true,
914+
help:
915+
'Additional (external) dart files to include; use "dir/fileName", '
916+
'as in lib/material.dart.',
917+
mustExist: true,
918+
splitCommas: true),
919+
new DartdocOptionBoth<bool>('includeSource', true,
920+
help: 'Show source code blocks.', negatable: true),
921+
new DartdocOptionArgOnly<String>('input', Directory.current.path,
922+
isDir: true, help: 'Path to source directory', mustExist: true),
923+
new DartdocOptionArgOnly<bool>('json', false,
924+
help: 'Prints out progress JSON maps. One entry per line.',
925+
negatable: true),
926+
new DartdocOptionArgOnly<String>('output', defaultOutDir,
927+
isDir: true, help: 'Path to output directory.'),
928+
new DartdocOptionBoth<List<String>>('packageOrder', [],
929+
help:
930+
'A list of package names to place first when grouping libraries in packages. '
931+
'Unmentioned categories are sorted after these.'),
932+
new DartdocOptionArgOnly<bool>('prettyIndexJson', false,
933+
help:
934+
"Generates `index.json` with indentation and newlines. The file is larger, but it's also easier to diff.",
935+
negatable: false),
936+
new DartdocOptionArgOnly<String>('relCanonicalPrefix', null,
937+
help:
938+
'If provided, add a rel="canonical" prefixed with provided value. '
939+
'Consider using if\nbuilding many versions of the docs for public '
940+
'SEO; learn more at https://goo.gl/gktN6F.'),
941+
new DartdocOptionArgOnly<bool>('sdk-docs', false,
942+
help: 'Generate ONLY the docs for the Dart SDK.', negatable: false),
943+
new DartdocOptionArgOnly<String>('sdk-dir', defaultSdkDir.absolute.path,
944+
help: 'Path to the SDK directory', isDir: true, mustExist: true),
945+
new DartdocOptionArgOnly<bool>('show-progress', false,
946+
help: 'Display progress indications to console stdout',
947+
negatable: false),
948+
new DartdocOptionArgOnly<bool>('show-warnings', false,
949+
help: 'Display all warnings.', negatable: false),
950+
new DartdocOptionArgOnly<bool>('use-categories', true,
951+
help: 'Display categories in the sidebar of packages',
952+
negatable: false),
953+
new DartdocOptionArgOnly<bool>('validate-links', true,
954+
help:
955+
'Runs the built-in link checker to display Dart context aware warnings for broken links (slow)',
956+
negatable: true),
957+
new DartdocOptionArgOnly<bool>('verbose-warnings', true,
958+
help: 'Display extra debugging information and help with warnings.',
959+
negatable: true),
960+
new DartdocOptionArgOnly<bool>('version', false,
961+
help: 'Display the version for $name.', negatable: false),
962+
]);
963+
}
964+
711965
final Map<String, DartdocOptions> _dartdocOptionsCache = {};
712966

713967
/// Legacy dartdoc options class. TODO(jcollins-g): merge with [DartdocOption].

test/dartdoc_options_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,20 @@ void main() {
9595
.add(new DartdocOptionBoth<String>('notInAnyFile', 'so there'));
9696
dartdocOptionSetBoth.add(new DartdocOptionBoth<String>('fileOption', null,
9797
isFile: true, mustExist: true));
98+
dartdocOptionSetBoth.add(new DartdocOptionSynthetic<List<String>>(
99+
'vegetableLoader', (DartdocOptionSynthetic option, Directory dir) {
100+
if (option.root['mySpecialInteger'].valueAt(dir) > 20) {
101+
return <String>['existing.dart'];
102+
} else {
103+
return <String>['not_existing.dart'];
104+
}
105+
}));
106+
dartdocOptionSetBoth.add(new DartdocOptionSynthetic<List<String>>(
107+
'vegetableLoaderChecked',
108+
(DartdocOptionSynthetic option, Directory dir) =>
109+
option.root['vegetableLoader'].valueAt(dir),
110+
isFile: true,
111+
mustExist: true));
98112

99113
tempDir = Directory.systemTemp.createTempSync('options_test');
100114
firstDir = new Directory(pathLib.join(tempDir.path, 'firstDir'))
@@ -152,6 +166,37 @@ dartdoc:
152166
tempDir.deleteSync(recursive: true);
153167
});
154168

169+
group('new style synthetic option', () {
170+
test('validate argument override changes value', () {
171+
dartdocOptionSetBoth.parseArguments(['--my-special-integer', '12']);
172+
expect(dartdocOptionSetBoth['vegetableLoader'].valueAt(tempDir),
173+
orderedEquals(['not_existing.dart']));
174+
});
175+
176+
test('validate default value of synthetic', () {
177+
dartdocOptionSetBoth.parseArguments([]);
178+
expect(dartdocOptionSetBoth['vegetableLoader'].valueAt(tempDir),
179+
orderedEquals(['existing.dart']));
180+
});
181+
182+
test('file validation of synthetic', () {
183+
dartdocOptionSetBoth.parseArguments([]);
184+
expect(dartdocOptionSetBoth['vegetableLoaderChecked'].valueAt(firstDir),
185+
orderedEquals([pathLib.canonicalize(firstExisting.path)]));
186+
187+
String errorMessage;
188+
try {
189+
dartdocOptionSetBoth['vegetableLoaderChecked'].valueAt(tempDir);
190+
} on DartdocFileMissing catch (e) {
191+
errorMessage = e.message;
192+
}
193+
expect(
194+
errorMessage,
195+
equals(
196+
'Synthetic configuration option dartdoc from <internal>, computed as [existing.dart], resolves to missing path: "${pathLib.canonicalize(pathLib.join(tempDir.absolute.path, 'existing.dart'))}"'));
197+
});
198+
});
199+
155200
group('new style dartdoc both file and argument options', () {
156201
test(
157202
'validate argument with wrong file throws error even if dartdoc_options is right',

tool/grind.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import 'dart:async';
6-
import 'dart:collection';
76
import 'dart:io' hide ProcessException;
87

98
import 'package:dartdoc/src/io_utils.dart';

0 commit comments

Comments
 (0)