diff --git a/.travis.yml b/.travis.yml index e61ecd38b2..794905e56d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,3 +15,4 @@ branches: cache: directories: - $HOME/.pub-cache + - $HOME/.dartdoc_grinder diff --git a/README.md b/README.md index d0fcbcfe14..029fc6ac87 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,14 @@ Unrecognized options will be ignored. Supported options: * **linkTo**: For other packages depending on this one, if this map is defined those packages will use the settings here to control how hyperlinks to the package are generated. This will override the default for packages hosted on pub.dartlang.org. - * url: A string indicating the base URL for documentation of this package. The following - strings will be substituted in to complete the URL: + * **url**: A string indicating the base URL for documentation of this package. Ordinarily + you do not need to set this in the package: consider --link-to-hosted and + --link-to-sdks instead of this option if you need to build your own website with + dartdoc. + + The following strings will be substituted in to complete the URL: + * `%b%`: The branch as indicated by text in the version. 2.0.0-dev.3 is branch "dev". + No branch is considered to be "stable". * `%n%`: The name of this package, as defined in pubspec.yaml. * `%v%`: The version of this package as defined in pubspec.yaml. diff --git a/analysis_options.yaml b/analysis_options.yaml index c421e35483..ee60de8645 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,7 @@ analyzer: - 'lib/templates/*.html' - 'pub.dartlang.org/**' - 'testing/**' + - 'testing/test_package_flutter_plugin/**' linter: rules: - annotate_overrides diff --git a/bin/dartdoc.dart b/bin/dartdoc.dart index 1984c7fbe8..583913e892 100644 --- a/bin/dartdoc.dart +++ b/bin/dartdoc.dart @@ -69,7 +69,7 @@ main(List arguments) async { logInfo("Generating documentation for '${config.topLevelPackageMeta}' into " "${outputDir.absolute.path}${Platform.pathSeparator}"); - Dartdoc dartdoc = await Dartdoc.withDefaultGenerators(config, outputDir); + Dartdoc dartdoc = await Dartdoc.withDefaultGenerators(config); dartdoc.onCheckProgress.listen(logProgress); await Chain.capture(() async { diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart index 00ca2aa6e1..e80abb61d4 100644 --- a/lib/dartdoc.dart +++ b/lib/dartdoc.dart @@ -43,32 +43,30 @@ const String dartdocVersion = '0.18.1'; /// directory. class Dartdoc extends PackageBuilder { final List generators; - final Directory outputDir; final Set writtenFiles = new Set(); + Directory outputDir; // Fires when the self checks make progress. final StreamController _onCheckProgress = new StreamController(sync: true); - Dartdoc._(DartdocOptionContext config, this.generators, this.outputDir) - : super(config) { + Dartdoc._(DartdocOptionContext config, this.generators) : super(config) { + outputDir = new Directory(config.output)..createSync(recursive: true); generators.forEach((g) => g.onFileCreated.listen(logProgress)); } /// An asynchronous factory method that builds Dartdoc's file writers /// and returns a Dartdoc object with them. - static withDefaultGenerators( - DartdocOptionContext config, Directory outputDir) async { + static withDefaultGenerators(DartdocOptionContext config) async { List generators = await initGenerators(config as GeneratorContext); - return new Dartdoc._(config, generators, outputDir); + return new Dartdoc._(config, generators); } /// Basic synchronous factory that gives a stripped down Dartdoc that won't /// use generators. Useful for testing. - factory Dartdoc.withoutGenerators( - DartdocOptionContext config, Directory outputDir) { - return new Dartdoc._(config, [], outputDir); + factory Dartdoc.withoutGenerators(DartdocOptionContext config) { + return new Dartdoc._(config, []); } Stream get onCheckProgress => _onCheckProgress.stream; @@ -345,25 +343,28 @@ class Dartdoc extends PackageBuilder { // (newPathToCheck, newFullPath) Set> toVisit = new Set(); + final RegExp ignoreHyperlinks = new RegExp(r'^(https:|http:|mailto:|ftp:)'); for (String href in stringLinks) { - Uri uri; - try { - uri = Uri.parse(href); - } catch (FormatError) {} - - if (uri == null || !uri.hasAuthority && !uri.hasFragment) { - var full; - if (baseHref != null) { - full = '${pathLib.dirname(pathToCheck)}/$baseHref/$href'; - } else { - full = '${pathLib.dirname(pathToCheck)}/$href'; - } - var newPathToCheck = pathLib.normalize(full); - String newFullPath = pathLib.joinAll([origin, newPathToCheck]); - newFullPath = pathLib.normalize(newFullPath); - if (!visited.contains(newFullPath)) { - toVisit.add(new Tuple2(newPathToCheck, newFullPath)); - visited.add(newFullPath); + if (!href.startsWith(ignoreHyperlinks)) { + Uri uri; + try { + uri = Uri.parse(href); + } catch (FormatError) {} + + if (uri == null || !uri.hasAuthority && !uri.hasFragment) { + var full; + if (baseHref != null) { + full = '${pathLib.dirname(pathToCheck)}/$baseHref/$href'; + } else { + full = '${pathLib.dirname(pathToCheck)}/$href'; + } + var newPathToCheck = pathLib.normalize(full); + String newFullPath = pathLib.joinAll([origin, newPathToCheck]); + newFullPath = pathLib.normalize(newFullPath); + if (!visited.contains(newFullPath)) { + toVisit.add(new Tuple2(newPathToCheck, newFullPath)); + visited.add(newFullPath); + } } } } diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart index 627bd9f36b..8ee26a6745 100644 --- a/lib/src/dartdoc_options.dart +++ b/lib/src/dartdoc_options.dart @@ -32,7 +32,7 @@ const int _kIntVal = 0; const double _kDoubleVal = 0.0; const bool _kBoolVal = true; -String _resolveTildePath(String originalPath) { +String resolveTildePath(String originalPath) { if (originalPath == null || !originalPath.startsWith('~/')) { return originalPath; } @@ -97,11 +97,11 @@ class _OptionValueWithContext { T get resolvedValue { if (value is List) { return (value as List) - .map((v) => pathContext.canonicalize(_resolveTildePath(v))) + .map((v) => pathContext.canonicalize(resolveTildePath(v))) .cast() .toList() as T; } else if (value is String) { - return pathContext.canonicalize(_resolveTildePath(value as String)) as T; + return pathContext.canonicalize(resolveTildePath(value as String)) as T; } else { throw new UnsupportedError('Type $T is not supported for resolvedValue'); } @@ -117,7 +117,7 @@ class _OptionValueWithContext { /// 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], [DartdocOptionBoth], +/// Use via implementations [DartdocOptionSet], [DartdocOptionArgFile], /// [DartdocOptionArgOnly], and [DartdocOptionFileOnly]. abstract class DartdocOption { /// This is the value returned if we couldn't find one otherwise. @@ -154,7 +154,6 @@ abstract class DartdocOption { // 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; @@ -181,7 +180,7 @@ abstract class DartdocOption { /// Parse these as string arguments (from argv) with the argument parser. /// Call before calling [valueAt] for any [DartdocOptionArgOnly] or - /// [DartdocOptionBoth] in this tree. + /// [DartdocOptionArgFile] in this tree. void _parseArguments(List arguments) { __argResults = argParser.parse(arguments); } @@ -303,23 +302,121 @@ abstract class DartdocOption { } } +/// A class that defaults to a value computed from a closure, but can be +/// overridden by a file. +class DartdocOptionFileSynth extends DartdocOption + with DartdocSyntheticOption, _DartdocFileOption { + bool _parentDirOverridesChild; + @override + T Function(DartdocSyntheticOption, Directory) _compute; + DartdocOptionFileSynth(String name, this._compute, + {bool mustExist = false, + String help = '', + bool isDir = false, + bool isFile = false, + bool parentDirOverridesChild}) + : super._(name, null, help, isDir, isFile, mustExist) { + _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 extends DartdocOption + with DartdocSyntheticOption, _DartdocArgOption { + String _abbr; + bool _hide; + bool _negatable; + bool _splitCommas; + + @override + T Function(DartdocSyntheticOption, Directory) _compute; + DartdocOptionArgSynth(String name, this._compute, + {String abbr, + bool mustExist = false, + String help = '', + bool isDir = false, + bool isFile = false, + bool negatable, + bool splitCommas}) + : super._(name, null, help, isDir, isFile, mustExist) { + _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 DartdocOptionSynthetic extends DartdocOption { - T Function(DartdocOptionSynthetic, Directory) _compute; - - DartdocOptionSynthetic(String name, this._compute, +class DartdocOptionSyntheticOnly extends DartdocOption + with DartdocSyntheticOption { + @override + T Function(DartdocSyntheticOption, 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); +} + +abstract class DartdocSyntheticOption implements DartdocOption { + T Function(DartdocSyntheticOption, Directory) get _compute; @override - T valueAt(Directory dir) { + T valueAt(Directory dir) => _valueAtFromSynthetic(dir); + + T _valueAtFromSynthetic(Directory dir) { _OptionValueWithContext context = new _OptionValueWithContext(_compute(this, dir), dir.path); return _handlePathsInContext(context); @@ -327,6 +424,10 @@ class DartdocOptionSynthetic extends DartdocOption { @override void _onMissing( + _OptionValueWithContext valueWithContext, String missingPath) => + _onMissingFromSynthetic(valueWithContext, missingPath); + + void _onMissingFromSynthetic( _OptionValueWithContext valueWithContext, String missingPath) { String description = 'Synthetic configuration option ${name} from '; @@ -409,7 +510,7 @@ class DartdocOptionArgOnly extends DartdocOption } /// A [DartdocOption] that works with command line arguments and dartdoc_options files. -class DartdocOptionBoth extends DartdocOption +class DartdocOptionArgFile extends DartdocOption with _DartdocArgOption, _DartdocFileOption { String _abbr; bool _hide; @@ -417,7 +518,7 @@ class DartdocOptionBoth extends DartdocOption bool _parentDirOverridesChild; bool _splitCommas; - DartdocOptionBoth(String name, T defaultsTo, + DartdocOptionArgFile(String name, T defaultsTo, {String abbr, bool mustExist = false, String help: '', @@ -438,24 +539,16 @@ class DartdocOptionBoth extends DartdocOption @override void _onMissing( _OptionValueWithContext valueWithContext, String missingPath) { - String dartdocYaml; - String description; if (valueWithContext.definingFile != null) { - dartdocYaml = pathLib.canonicalize(pathLib.join( - valueWithContext.canonicalDirectoryPath, - valueWithContext.definingFile)); - description = 'Field ${fieldName} from ${dartdocYaml}'; + _onMissingFromFiles(valueWithContext, missingPath); } else { - description = 'Argument --${argName}'; + _onMissingFromArgs(valueWithContext, missingPath); } - throw new DartdocFileMissing( - '$description, set to ${valueWithContext.value}, resolves to missing path: "${missingPath}"'); } - @override - /// 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); @@ -488,16 +581,6 @@ class DartdocOptionFileOnly extends DartdocOption _parentDirOverridesChild = parentDirOverridesChild; } - @override - void _onMissing( - _OptionValueWithContext valueWithContext, String missingPath) { - String dartdocYaml = pathLib.canonicalize(pathLib.join( - valueWithContext.canonicalDirectoryPath, - valueWithContext.definingFile)); - throw new DartdocFileMissing( - 'Field ${fieldName} from ${dartdocYaml}, set to ${valueWithContext.value}, resolves to missing path: "${missingPath}"'); - } - @override bool get parentDirOverridesChild => _parentDirOverridesChild; } @@ -520,6 +603,10 @@ abstract class _DartdocFileOption implements DartdocOption { @override void _onMissing( + _OptionValueWithContext valueWithContext, String missingPath) => + _onMissingFromFiles(valueWithContext, missingPath); + + void _onMissingFromFiles( _OptionValueWithContext valueWithContext, String missingPath) { String dartdocYaml = pathLib.join( valueWithContext.canonicalDirectoryPath, valueWithContext.definingFile); @@ -643,16 +730,16 @@ abstract class _DartdocFileOption implements DartdocOption { /// Mixin class implementing command-line arguments for [DartdocOption]. abstract class _DartdocArgOption implements DartdocOption { - /// For [ArgsParser], set to true if the argument can be negated with --no on the command line. + /// For [ArgParser], set to true if the argument can be negated with --no on the command line. bool get negatable; - /// For [ArgsParser], set to true if a single string argument will be broken into a list on commas. + /// For [ArgParser], set to true if a single string argument will be broken into a list on commas. bool get splitCommas; - /// For [ArgsParser], set to true to hide this from the help menu. + /// For [ArgParser], set to true to hide this from the help menu. bool get hide; - /// For [ArgsParser], set to a single character to have a short version of the command line argument. + /// 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 @@ -682,6 +769,10 @@ abstract class _DartdocArgOption implements DartdocOption { @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}"'); @@ -757,7 +848,7 @@ abstract class _DartdocArgOption implements DartdocOption { } else if (_isInt || _isDouble || _isString) { argParser.addOption(argName, abbr: abbr, - defaultsTo: defaultsTo.toString(), + defaultsTo: defaultsTo?.toString() ?? null, help: help, hide: hide); } else if (_isListString || _isMapString) { @@ -832,6 +923,7 @@ class DartdocOptionContext { List get excludePackages => optionSet['excludePackages'].valueAt(context); + String get flutterRoot => optionSet['flutterRoot'].valueAt(context); bool get hideSdkText => optionSet['hideSdkText'].valueAt(context); List get include => optionSet['include'].valueAt(context); List get includeExternal => @@ -842,17 +934,13 @@ class DartdocOptionContext { // ignore: unused_element String get _input => optionSet['input'].valueAt(context); String get inputDir => optionSet['inputDir'].valueAt(context); - bool get linkToExternal => optionSet['linkTo']['external'].valueAt(context); - String get linkToExternalUrl => - optionSet['linkTo']['externalUrl'].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); - /// _linkToUrl is only used to construct synthetic options. - // ignore: unused_element - String get _linkToUrl => optionSet['linkTo']['url'].valueAt(context); String get output => optionSet['output'].valueAt(context); PackageMeta get packageMeta => optionSet['packageMeta'].valueAt(context); List get packageOrder => optionSet['packageOrder'].valueAt(context); @@ -878,19 +966,20 @@ Future> createDartdocOptions() async { new DartdocOptionArgOnly('addCrossdart', false, help: 'Add Crossdart links to the source code pieces.', negatable: true), - new DartdocOptionBoth('ambiguousReexportScorerMinConfidence', 0.1, + new DartdocOptionArgFile( + 'ambiguousReexportScorerMinConfidence', 0.1, help: 'Minimum scorer confidence to suppress warning on ambiguous reexport.'), new DartdocOptionArgOnly('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 DartdocOptionBoth>('categoryOrder', [], + new DartdocOptionArgFile>('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 DartdocOptionSynthetic>('dropTextFrom', - (DartdocOptionSynthetic option, Directory dir) { + new DartdocOptionSyntheticOnly>('dropTextFrom', + (DartdocSyntheticOption> option, Directory dir) { if (option.parent['hideSdkText'].valueAt(dir)) { return [ 'dart.async', @@ -913,22 +1002,32 @@ Future> createDartdocOptions() async { } return []; }, help: 'Remove text from libraries with the following names.'), - new DartdocOptionBoth('examplePathPrefix', null, + new DartdocOptionArgFile('examplePathPrefix', null, isDir: true, help: 'Prefix for @example paths.\n(defaults to the project root)', mustExist: true), - new DartdocOptionBoth>('exclude', [], + new DartdocOptionArgFile>('exclude', [], help: 'Library names to ignore.', splitCommas: true), new DartdocOptionArgOnly>('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( + 'flutterRoot', + (DartdocSyntheticOption option, Directory dir) => + resolveTildePath(Platform.environment['FLUTTER_ROOT']), + isDir: true, + help: 'Root of the Flutter SDK, specified from environment.', + mustExist: true, + ), new DartdocOptionArgOnly('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 DartdocOptionBoth>('include', [], + new DartdocOptionArgFile>('include', [], help: 'Library names to generate docs for.', splitCommas: true), - new DartdocOptionBoth>('includeExternal', null, + new DartdocOptionArgFile>('includeExternal', null, isFile: true, help: 'Additional (external) dart files to include; use "dir/fileName", ' @@ -939,8 +1038,8 @@ Future> createDartdocOptions() async { help: 'Show source code blocks.', negatable: true), new DartdocOptionArgOnly('input', Directory.current.path, isDir: true, help: 'Path to source directory', mustExist: true), - new DartdocOptionSynthetic('inputDir', - (DartdocOptionSynthetic option, Directory dir) { + new DartdocOptionSyntheticOnly('inputDir', + (DartdocSyntheticOption option, Directory dir) { if (option.parent['sdkDocs'].valueAt(dir)) { return option.parent['sdkDir'].valueAt(dir); } @@ -951,23 +1050,6 @@ Future> createDartdocOptions() async { mustExist: true), new DartdocOptionSet('linkTo') ..addAll([ - new DartdocOptionArgOnly('external', false, - help: 'Allow links to be generated for packages outside this one.', - negatable: true), - new DartdocOptionSynthetic('externalUrl', - (DartdocOptionSynthetic option, Directory dir) { - String url = option.parent['url'].valueAt(dir); - if (url != null) { - return url; - } - String hostedAt = - option.parent.parent['packageMeta'].valueAt(dir).hostedAt; - if (hostedAt != null) { - Map hostMap = option.parent['hosted'].valueAt(dir); - if (hostMap.containsKey(hostedAt)) return hostMap[hostedAt]; - } - return ''; - }, help: 'Url to use for this particular package.'), new DartdocOptionArgOnly>( 'hosted', { @@ -975,14 +1057,41 @@ Future> createDartdocOptions() async { 'https://pub.dartlang.org/documentation/%n%/%v%' }, help: 'Specify URLs for hosted pub packages'), - new DartdocOptionFileOnly('url', null, - help: 'Use external linkage for this package, with this base url'), + new DartdocOptionArgOnly>( + 'sdks', + { + 'Dart': 'https://api.dartlang.org/%b%/%v%', + 'Flutter': 'https://docs.flutter.io/flutter', + }, + help: 'Specify URLs for SDKs.', + ), + new DartdocOptionFileSynth('url', + (DartdocSyntheticOption 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 sdks = option.parent['sdks'].valueAt(dir); + if (sdks.containsKey(inSdk)) return sdks[inSdk]; + } + String hostedAt = packageMeta.hostedAt; + if (hostedAt != null) { + Map hostMap = option.parent['hosted'].valueAt(dir); + if (hostMap.containsKey(hostedAt)) return hostMap[hostedAt]; + } + return ''; + }, help: 'Url to use for this particular package.'), + new DartdocOptionArgOnly('remote', false, + help: 'Allow links to be generated for packages outside this one.', + negatable: true), ]), new DartdocOptionArgOnly('output', pathLib.join('doc', 'api'), isDir: true, help: 'Path to output directory.'), - new DartdocOptionSynthetic( + new DartdocOptionSyntheticOnly( 'packageMeta', - (DartdocOptionSynthetic option, Directory dir) { + (DartdocSyntheticOption option, Directory dir) { PackageMeta packageMeta = new PackageMeta.fromDir(dir); if (packageMeta == null) { throw new DartdocOptionError( @@ -997,12 +1106,20 @@ Future> createDartdocOptions() async { 'Unmentioned categories are sorted after these.'), new DartdocOptionArgOnly('sdkDocs', false, help: 'Generate ONLY the docs for the Dart SDK.', negatable: false), - new DartdocOptionArgOnly('sdkDir', defaultSdkDir.absolute.path, - help: 'Path to the SDK directory.', isDir: true, mustExist: true), + new DartdocOptionArgSynth('sdkDir', + (DartdocSyntheticOption option, Directory dir) { + if (!option.parent['sdkDocs'].valueAt(dir) && + (option.root['topLevelPackageMeta'].valueAt(dir) as PackageMeta) + .requiresFlutter) { + return pathLib.join(option.root['flutterRoot'].valueAt(dir), 'bin', + 'cache', 'dart-sdk'); + } + return defaultSdkDir.absolute.path; + }, help: 'Path to the SDK directory.', isDir: true, mustExist: true), new DartdocOptionArgOnly('showWarnings', false, help: 'Display all warnings.', negatable: false), - new DartdocOptionSynthetic('topLevelPackageMeta', - (DartdocOptionSynthetic option, Directory dir) { + new DartdocOptionSyntheticOnly('topLevelPackageMeta', + (DartdocSyntheticOption option, Directory dir) { PackageMeta packageMeta = new PackageMeta.fromDir( new Directory(option.parent['inputDir'].valueAt(dir))); if (packageMeta == null) { diff --git a/lib/src/html/html_generator.dart b/lib/src/html/html_generator.dart index 1210cf4da6..f7060601b5 100644 --- a/lib/src/html/html_generator.dart +++ b/lib/src/html/html_generator.dart @@ -179,33 +179,41 @@ abstract class GeneratorContext implements DartdocOptionContext { Future> createGeneratorOptions() async { await _setSdkFooterCopyrightUri(); return [ - new DartdocOptionBoth('favicon', null, + new DartdocOptionArgFile('favicon', null, isFile: true, help: 'A path to a favicon for the generated docs.', mustExist: true), - new DartdocOptionBoth>('footer', [], + new DartdocOptionArgFile>('footer', [], isFile: true, help: 'paths to footer files containing HTML text.', mustExist: true, splitCommas: true), - new DartdocOptionBoth>('footerText', [], + new DartdocOptionArgFile>('footerText', [], isFile: true, help: 'paths to footer-text files (optional text next to the package name ' 'and version).', mustExist: true, splitCommas: true), - new DartdocOptionSynthetic>('footerTextPaths', - (DartdocOptionSynthetic option, Directory dir) { - List footerTextPaths = []; - // TODO(jcollins-g): Eliminate special casing for SDK and use config file. - if (new PackageMeta.fromDir(dir).isSdk) { - footerTextPaths.add(_sdkFooterCopyrightUri.toFilePath()); - } - footerTextPaths.addAll(option.parent['footerText'].valueAt(dir)); - return footerTextPaths; - }), - new DartdocOptionBoth>('header', [], + new DartdocOptionSyntheticOnly>( + 'footerTextPaths', + (DartdocSyntheticOption> option, Directory dir) { + final List footerTextPaths = []; + final PackageMeta topLevelPackageMeta = + option.root['topLevelPackageMeta'].valueAt(dir); + // TODO(jcollins-g): Eliminate special casing for SDK and use config file. + if (topLevelPackageMeta.isSdk == true) { + footerTextPaths + .add(pathLib.canonicalize(_sdkFooterCopyrightUri.toFilePath())); + } + footerTextPaths.addAll(option.parent['footerText'].valueAt(dir)); + return footerTextPaths; + }, + isFile: true, + help: 'paths to footer-text-files (adding special case for SDK)', + mustExist: true, + ), + new DartdocOptionArgFile>('header', [], isFile: true, help: 'paths to header files containing HTML text.', splitCommas: true), diff --git a/lib/src/io_utils.dart b/lib/src/io_utils.dart index d91f44164b..a97da00b83 100644 --- a/lib/src/io_utils.dart +++ b/lib/src/io_utils.dart @@ -66,7 +66,7 @@ final newLinePartOfRegexp = new RegExp('\npart of '); final RegExp quotables = new RegExp(r'[ "\r\n\$]'); -/// Best used with Future. +/// Best used with Future. class MultiFutureTracker { /// Approximate maximum number of simultaneous active Futures. final int parallel; @@ -76,13 +76,15 @@ class MultiFutureTracker { MultiFutureTracker(this.parallel); /// Adds a Future to the queue of outstanding Futures, and returns a Future - /// that completes only when the number of Futures outstanding is <= parallel. + /// that completes only when the number of Futures outstanding is < [parallel] + /// (and so it is OK to start another). + /// /// That can be extremely brief and there's no longer a guarantee after that /// point that another async task has not added a Future to the list. void addFuture(Future future) async { _queue.add(future); future.then((f) => _queue.remove(future)); - await _waitUntil(parallel); + await _waitUntil(parallel - 1); } /// Wait until fewer or equal to this many Futures are outstanding. @@ -103,11 +105,11 @@ class SubprocessLauncher { String get prefix => context.isNotEmpty ? '$context: ' : ''; // from flutter:dev/tools/dartdoc.dart, modified - static void _printStream(Stream> stream, Stdout output, + static Future _printStream(Stream> stream, Stdout output, {String prefix: '', Iterable Function(String line) filter}) { assert(prefix != null); if (filter == null) filter = (line) => [line]; - stream + return stream .transform(utf8.decoder) .transform(const LineSplitter()) .expand(filter) @@ -116,7 +118,7 @@ class SubprocessLauncher { output.write('$prefix$line'.trim()); output.write('\n'); } - }); + }).asFuture(); } SubprocessLauncher(this.context, [Map environment]) @@ -186,9 +188,11 @@ class SubprocessLauncher { Process process = await Process.start(executable, arguments, workingDirectory: workingDirectory, environment: environment); - _printStream(process.stdout, stdout, prefix: prefix, filter: jsonCallback); - _printStream(process.stderr, stderr, prefix: prefix, filter: jsonCallback); - await process.exitCode; + Future stdoutFuture = _printStream(process.stdout, stdout, + prefix: prefix, filter: jsonCallback); + Future stderrFuture = _printStream(process.stderr, stderr, + prefix: prefix, filter: jsonCallback); + await Future.wait([stderrFuture, stdoutFuture, process.exitCode]); int exitCode = await process.exitCode; if (exitCode != 0) { diff --git a/lib/src/model.dart b/lib/src/model.dart index 8f437e4d79..78c5a26f2f 100644 --- a/lib/src/model.dart +++ b/lib/src/model.dart @@ -48,6 +48,7 @@ import 'package:dartdoc/src/warnings.dart'; import 'package:front_end/src/byte_store/byte_store.dart'; import 'package:front_end/src/base/performance_logger.dart'; import 'package:path/path.dart' as pathLib; +import 'package:pub_semver/pub_semver.dart'; import 'package:tuple/tuple.dart'; import 'package:package_config/discovery.dart' as package_config; @@ -79,8 +80,8 @@ int byFeatureOrdering(String a, String b) { return compareAsciiLowerCaseNatural(a, b); } -final RegExp locationSplitter = new RegExp(r"(package:|[\\/;.])"); -final RegExp substituteName = new RegExp(r"%([nv])%"); +final RegExp locationSplitter = new RegExp(r'(package:|[\\/;.])'); +final RegExp substituteNameVersion = new RegExp(r'%([bnv])%'); /// Mixin for subclasses of ModelElement representing Elements that can be /// inherited from one class to another. @@ -544,7 +545,7 @@ class Class extends ModelElement } /// This class might be canonical for elements it does not contain. - /// See [canonicalEnclosingElement]. + /// See [Inheritable.canonicalEnclosingElement]. bool contains(Element element) => allElements.containsKey(element); ModelElement findModelElement(Element element) => allElements[element]; @@ -1755,7 +1756,7 @@ class Library extends ModelElement with Categorization { /// [allModelElements] resolved to their original names. /// - /// A collection of [ModelElement.fullyQualifiedNames] for [ModelElement]s + /// A collection of [ModelElement.fullyQualifiedName]s for [ModelElement]s /// documented with this library, but these ModelElements and names correspond /// to the defining library where each originally came from with respect /// to inheritance and reexporting. Most useful for error reporting. @@ -3463,11 +3464,11 @@ abstract class ModelElement extends Canonicalization return lib; } - /// Replace {@example ...} in API comments with the content of named file. + /// Replace {@example ...} in API comments with the content of named file. /// /// Syntax: /// - /// {@example PATH [region=NAME] [lang=NAME]} + /// {@example PATH [region=NAME] [lang=NAME]} /// /// where PATH and NAME are tokens _without_ whitespace; NAME can optionally be /// quoted (use of quotes is for backwards compatibility and discouraged). @@ -3476,10 +3477,11 @@ abstract class ModelElement extends Canonicalization /// named `dir/file-r.ext.md`, relative to the project root directory (of the /// project for which the docs are being generated). /// - /// Examples: + /// Examples: (escaped in this comment to show literal values in dartdoc's + /// dartdoc) /// - /// {@example examples/angular/quickstart/web/main.dart} - /// {@example abc/def/xyz_component.dart region=template lang=html} + /// {@example examples/angular/quickstart/web/main.dart} + /// {@example abc/def/xyz_component.dart region=template lang=html} /// String _injectExamples(String rawdocs) { final dirPath = package.packageMeta.dir.path; @@ -3621,7 +3623,7 @@ class ModelFunction extends ModelFunctionTyped { FunctionElement get _func => (element as FunctionElement); } -/// A [ModelElement] for a [GenericModelFunctionElement] that is an +/// A [ModelElement] for a [FunctionTypedElement] that is an /// explicit typedef. /// /// Distinct from ModelFunctionTypedef in that it doesn't @@ -3642,7 +3644,7 @@ class ModelFunctionAnonymous extends ModelFunctionTyped { bool get isPublic => false; } -/// A [ModelElement] for a [GenericModelFunctionElement] that is part of an +/// A [ModelElement] for a [FunctionTypedElement] that is part of an /// explicit typedef. class ModelFunctionTypedef extends ModelFunctionTyped { ModelFunctionTypedef( @@ -3948,7 +3950,7 @@ class PackageGraph extends Canonicalization String get location => '(top level package)'; /// Flush out any warnings we might have collected while - /// [_packageWarningOptions.autoFlush] was false. + /// [PackageWarningOptions.autoFlush] was false. void flushWarnings() { _packageWarningCounter.maybeFlush(); } @@ -4669,7 +4671,7 @@ abstract class LibraryContainer extends Nameable /// A category is a subcategory of a package, containing libraries tagged /// with a @category identifier. Comparable so it can be sorted according to -/// [config.categoryOrder]. +/// [DartdocOptionContext.categoryOrder]. class Category extends LibraryContainer { final String _name; @@ -4773,7 +4775,7 @@ class Package extends LibraryContainer DocumentLocation get documentedWhere { if (!isLocal) { - if (config.linkToExternal && config.linkToExternalUrl.isNotEmpty) { + if (config.linkToRemote && config.linkToUrl.isNotEmpty) { return DocumentLocation.remote; } else { return DocumentLocation.missing; @@ -4793,10 +4795,22 @@ class Package extends LibraryContainer if (_baseHref == null) { if (documentedWhere == DocumentLocation.remote) { _baseHref = - config.linkToExternalUrl.replaceAllMapped(substituteName, (m) { + config.linkToUrl.replaceAllMapped(substituteNameVersion, (m) { switch (m.group(1)) { + // Return the prerelease tag of the release if a prerelease, + // or 'stable' otherwise. Mostly coded around + // the Dart SDK's use of dev/stable, but theoretically applicable + // elsewhere. + case 'b': + { + Version version = new Version.parse(packageMeta.version); + return version.isPreRelease + ? version.preRelease.first + : 'stable'; + } case 'n': return name; + // The full version string of the package. case 'v': return packageMeta.version; } diff --git a/lib/src/package_meta.dart b/lib/src/package_meta.dart index fe44179daa..a2949b984b 100644 --- a/lib/src/package_meta.dart +++ b/lib/src/package_meta.dart @@ -132,9 +132,34 @@ abstract class PackageMeta { return _packageMetaCache[dir.absolute.path]; } + /// Returns true if this represents a 'Dart' SDK. A package can be part of + /// Dart and Flutter at the same time, but if we are part of a Dart SDK + /// sdkType should never return null. bool get isSdk; + + /// Returns 'Dart' or 'Flutter' (preferentially, 'Flutter' when the answer is + /// "both"), or null if this package is not part of a SDK. + String sdkType(String flutterRootPath) { + if (flutterRootPath != null) { + String flutterPackages = pathLib.join(flutterRootPath, 'packages'); + String flutterBinCache = pathLib.join(flutterRootPath, 'bin', 'cache'); + + /// Don't include examples or other non-SDK components as being the + /// "Flutter SDK". + if (pathLib.isWithin( + flutterPackages, pathLib.canonicalize(dir.absolute.path)) || + pathLib.isWithin( + flutterBinCache, pathLib.canonicalize(dir.absolute.path))) { + return 'Flutter'; + } + } + return isSdk ? 'Dart' : null; + } + bool get needsPubGet => false; + bool get requiresFlutter; + void runPubGet(); String get name; @@ -255,7 +280,7 @@ class _FilePackageMeta extends PackageMeta { StringBuffer buf = new StringBuffer(); buf.writeln('${result.stdout}'); buf.writeln('${result.stderr}'); - throw DartdocFailure('pub get failed: ${buf.toString().trim()}'); + throw new DartdocFailure('pub get failed: ${buf.toString().trim()}'); } } @@ -268,6 +293,10 @@ class _FilePackageMeta extends PackageMeta { @override String get homepage => _pubspec['homepage']; + @override + bool get requiresFlutter => + _pubspec['environment']?.containsKey('flutter') == true; + @override FileContents getReadmeContents() { if (_readme != null) return _readme; @@ -355,6 +384,9 @@ class _SdkMeta extends PackageMeta { @override String get homepage => 'https://github.com/dart-lang/sdk'; + @override + bool get requiresFlutter => false; + @override FileContents getReadmeContents() { File f = new File(pathLib.join(dir.path, 'lib', 'api_readme.md')); diff --git a/pubspec.lock b/pubspec.lock index a9a173a5fd..c82cf1df3f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -247,12 +247,12 @@ packages: source: hosted version: "1.3.4" pub_semver: - dependency: "direct dev" + dependency: "direct main" description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.3.2" + version: "1.3.7" quiver: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c2b0dc0b3a..b4a71c9a05 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: mustache4dart: ^2.1.1 package_config: '>=0.1.5 <2.0.0' path: ^1.3.0 + pub_semver: ^1.3.7 quiver: ^0.27.0 resource: ^2.1.2 stack_trace: ^1.4.2 @@ -33,7 +34,6 @@ dev_dependencies: io: ^0.3.0 http: ^0.11.0 meta: ^1.0.0 - pub_semver: ^1.0.0 test: '^0.12.24' executables: dartdoc: null diff --git a/test/dartdoc_options_test.dart b/test/dartdoc_options_test.dart index 0390ca2d61..a3b4fdd85d 100644 --- a/test/dartdoc_options_test.dart +++ b/test/dartdoc_options_test.dart @@ -13,7 +13,8 @@ import 'package:test/test.dart'; void main() { DartdocOptionSet dartdocOptionSetFiles; DartdocOptionSet dartdocOptionSetArgs; - DartdocOptionSet dartdocOptionSetBoth; + DartdocOptionSet dartdocOptionSetAll; + DartdocOptionSet dartdocOptionSetSynthetic; Directory tempDir; Directory firstDir; Directory secondDir; @@ -26,6 +27,33 @@ void main() { File firstExisting; setUpAll(() { + dartdocOptionSetSynthetic = new DartdocOptionSet('dartdoc'); + dartdocOptionSetSynthetic + .add(new DartdocOptionArgFile('mySpecialInteger', 91)); + dartdocOptionSetSynthetic.add( + new DartdocOptionSyntheticOnly>('vegetableLoader', + (DartdocSyntheticOption> option, Directory dir) { + if (option.root['mySpecialInteger'].valueAt(dir) > 20) { + return ['existing.dart']; + } else { + return ['not_existing.dart']; + } + })); + dartdocOptionSetSynthetic.add( + new DartdocOptionSyntheticOnly>('vegetableLoaderChecked', + (DartdocSyntheticOption> option, Directory dir) { + return option.root['vegetableLoader'].valueAt(dir); + }, isFile: true, mustExist: true)); + dartdocOptionSetSynthetic.add(new DartdocOptionFileSynth('double', + (DartdocSyntheticOption option, Directory dir) { + return 3.7 + 4.1; + })); + dartdocOptionSetSynthetic.add( + new DartdocOptionArgSynth('nonCriticalFileOption', + (DartdocSyntheticOption option, Directory dir) { + return option.root['vegetableLoader'].valueAt(dir).first; + }, isFile: true)); + dartdocOptionSetFiles = new DartdocOptionSet('dartdoc'); dartdocOptionSetFiles .add(new DartdocOptionFileOnly>('categoryOrder', [])); @@ -82,33 +110,20 @@ void main() { 'unimportantFile', 'whatever', isFile: true)); - dartdocOptionSetBoth = new DartdocOptionSet('dartdoc'); - dartdocOptionSetBoth - .add(new DartdocOptionBoth>('categoryOrder', [])); - dartdocOptionSetBoth - .add(new DartdocOptionBoth('mySpecialInteger', 91)); - dartdocOptionSetBoth.add(new DartdocOptionSet('warn') - ..addAll([new DartdocOptionBoth('unrecognizedVegetable', false)])); - dartdocOptionSetBoth.add(new DartdocOptionBoth>( + dartdocOptionSetAll = new DartdocOptionSet('dartdoc'); + dartdocOptionSetAll + .add(new DartdocOptionArgFile>('categoryOrder', [])); + dartdocOptionSetAll + .add(new DartdocOptionArgFile('mySpecialInteger', 91)); + dartdocOptionSetAll.add(new DartdocOptionSet('warn') + ..addAll( + [new DartdocOptionArgFile('unrecognizedVegetable', false)])); + dartdocOptionSetAll.add(new DartdocOptionArgFile>( 'mapOption', {'hi': 'there'})); - dartdocOptionSetBoth - .add(new DartdocOptionBoth('notInAnyFile', 'so there')); - dartdocOptionSetBoth.add(new DartdocOptionBoth('fileOption', null, + dartdocOptionSetAll + .add(new DartdocOptionArgFile('notInAnyFile', 'so there')); + dartdocOptionSetAll.add(new DartdocOptionArgFile('fileOption', null, isFile: true, mustExist: true)); - dartdocOptionSetBoth.add(new DartdocOptionSynthetic>( - 'vegetableLoader', (DartdocOptionSynthetic option, Directory dir) { - if (option.root['mySpecialInteger'].valueAt(dir) > 20) { - return ['existing.dart']; - } else { - return ['not_existing.dart']; - } - })); - dartdocOptionSetBoth.add(new DartdocOptionSynthetic>( - 'vegetableLoaderChecked', - (DartdocOptionSynthetic option, Directory dir) => - option.root['vegetableLoader'].valueAt(dir), - isFile: true, - mustExist: true)); tempDir = Directory.systemTemp.createTempSync('options_test'); firstDir = new Directory(pathLib.join(tempDir.path, 'firstDir')) @@ -168,25 +183,26 @@ dartdoc: group('new style synthetic option', () { test('validate argument override changes value', () { - dartdocOptionSetBoth.parseArguments(['--my-special-integer', '12']); - expect(dartdocOptionSetBoth['vegetableLoader'].valueAt(tempDir), + dartdocOptionSetSynthetic.parseArguments(['--my-special-integer', '12']); + expect(dartdocOptionSetSynthetic['vegetableLoader'].valueAt(tempDir), orderedEquals(['not_existing.dart'])); }); test('validate default value of synthetic', () { - dartdocOptionSetBoth.parseArguments([]); - expect(dartdocOptionSetBoth['vegetableLoader'].valueAt(tempDir), + dartdocOptionSetSynthetic.parseArguments([]); + expect(dartdocOptionSetSynthetic['vegetableLoader'].valueAt(tempDir), orderedEquals(['existing.dart'])); }); test('file validation of synthetic', () { - dartdocOptionSetBoth.parseArguments([]); - expect(dartdocOptionSetBoth['vegetableLoaderChecked'].valueAt(firstDir), + dartdocOptionSetSynthetic.parseArguments([]); + expect( + dartdocOptionSetSynthetic['vegetableLoaderChecked'].valueAt(firstDir), orderedEquals([pathLib.canonicalize(firstExisting.path)])); String errorMessage; try { - dartdocOptionSetBoth['vegetableLoaderChecked'].valueAt(tempDir); + dartdocOptionSetSynthetic['vegetableLoaderChecked'].valueAt(tempDir); } on DartdocFileMissing catch (e) { errorMessage = e.message; } @@ -195,17 +211,43 @@ dartdoc: equals( 'Synthetic configuration option dartdoc from , computed as [existing.dart], resolves to missing path: "${pathLib.canonicalize(pathLib.join(tempDir.absolute.path, 'existing.dart'))}"')); }); + + test('file can override synthetic in FileSynth', () { + dartdocOptionSetSynthetic.parseArguments([]); + expect( + dartdocOptionSetSynthetic['double'].valueAt(firstDir), equals(3.3)); + expect(dartdocOptionSetSynthetic['double'].valueAt(tempDir), equals(7.8)); + }); + + test('arg can override synthetic in ArgSynth', () { + dartdocOptionSetSynthetic + .parseArguments(['--non-critical-file-option', 'stuff.zip']); + // Since this is an ArgSynth, it ignores the yaml option and resolves to the CWD + expect( + dartdocOptionSetSynthetic['nonCriticalFileOption'].valueAt(firstDir), + equals(pathLib.canonicalize( + pathLib.join(Directory.current.path, 'stuff.zip')))); + }); + + test('ArgSynth defaults to synthetic', () { + dartdocOptionSetSynthetic.parseArguments([]); + // This option is composed of FileOptions which make use of firstDir. + expect( + dartdocOptionSetSynthetic['nonCriticalFileOption'].valueAt(firstDir), + equals(pathLib + .canonicalize(pathLib.join(firstDir.path, 'existing.dart')))); + }); }); group('new style dartdoc both file and argument options', () { test( 'validate argument with wrong file throws error even if dartdoc_options is right', () { - dartdocOptionSetBoth + dartdocOptionSetAll .parseArguments(['--file-option', 'override-not-existing.dart']); String errorMessage; try { - dartdocOptionSetBoth['fileOption'].valueAt(firstDir); + dartdocOptionSetAll['fileOption'].valueAt(firstDir); } on DartdocFileMissing catch (e) { errorMessage = e.message; } @@ -216,17 +258,17 @@ dartdoc: }); test('validate argument can override missing file', () { - dartdocOptionSetBoth.parseArguments( + dartdocOptionSetAll.parseArguments( ['--file-option', pathLib.canonicalize(firstExisting.path)]); - expect(dartdocOptionSetBoth['fileOption'].valueAt(secondDir), + expect(dartdocOptionSetAll['fileOption'].valueAt(secondDir), equals(pathLib.canonicalize(firstExisting.path))); }); test('File errors still get passed through', () { - dartdocOptionSetBoth.parseArguments([]); + dartdocOptionSetAll.parseArguments([]); String errorMessage; try { - dartdocOptionSetBoth['fileOption'].valueAt(secondDir); + dartdocOptionSetAll['fileOption'].valueAt(secondDir); } on DartdocFileMissing catch (e) { errorMessage = e.message; } @@ -238,36 +280,35 @@ dartdoc: }); test('validate override behavior basic', () { - dartdocOptionSetBoth.parseArguments( + dartdocOptionSetAll.parseArguments( ['--not-in-any-file', 'aha', '--map-option', 'over::theSea']); - expect(dartdocOptionSetBoth['mapOption'].valueAt(tempDir), + expect(dartdocOptionSetAll['mapOption'].valueAt(tempDir), equals({'over': 'theSea'})); - expect(dartdocOptionSetBoth['mapOption'].valueAt(firstDir), + expect(dartdocOptionSetAll['mapOption'].valueAt(firstDir), equals({'over': 'theSea'})); - expect(dartdocOptionSetBoth['notInAnyFile'].valueAt(firstDir), - equals('aha')); - expect(dartdocOptionSetBoth['mySpecialInteger'].valueAt(firstDir), + expect( + dartdocOptionSetAll['notInAnyFile'].valueAt(firstDir), equals('aha')); + expect(dartdocOptionSetAll['mySpecialInteger'].valueAt(firstDir), equals(30)); }); test('validate override behavior for parent directories', () { - dartdocOptionSetBoth.parseArguments(['--my-special-integer', '14']); - expect( - dartdocOptionSetBoth['mySpecialInteger'].valueAt(secondDirFirstSub), + dartdocOptionSetAll.parseArguments(['--my-special-integer', '14']); + expect(dartdocOptionSetAll['mySpecialInteger'].valueAt(secondDirFirstSub), equals(14)); }); test('validate arg defaults do not override file', () { - dartdocOptionSetBoth.parseArguments([]); - expect(dartdocOptionSetBoth['mySpecialInteger'].valueAt(secondDir), + dartdocOptionSetAll.parseArguments([]); + expect(dartdocOptionSetAll['mySpecialInteger'].valueAt(secondDir), equals(11)); }); test( 'validate setting the default manually in an argument overrides the file', () { - dartdocOptionSetBoth.parseArguments(['--my-special-integer', '91']); - expect(dartdocOptionSetBoth['mySpecialInteger'].valueAt(secondDir), + dartdocOptionSetAll.parseArguments(['--my-special-integer', '91']); + expect(dartdocOptionSetAll['mySpecialInteger'].valueAt(secondDir), equals(91)); }); }); diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart index 72b57150cf..6499e38dcf 100644 --- a/test/dartdoc_test.dart +++ b/test/dartdoc_test.dart @@ -4,6 +4,7 @@ library dartdoc.dartdoc_test; +import 'dart:async'; import 'dart:io'; import 'package:dartdoc/dartdoc.dart'; @@ -17,20 +18,23 @@ import 'src/utils.dart'; void main() { group('dartdoc without generators', () { Directory tempDir; - + List outputParam; setUp(() { tempDir = Directory.systemTemp.createTempSync('dartdoc.test.'); + outputParam = ['--output', tempDir.path]; }); tearDown(() { delete(tempDir); }); + Future contextFromArgvTemp(List argv) async { + return await contextFromArgv(argv..addAll(outputParam)); + } + test('basic interlinking test', () async { - Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv( - ['--input', testPackageDir.path, '--link-to-external']), - tempDir); + Dartdoc dartdoc = new Dartdoc.withoutGenerators(await contextFromArgvTemp( + ['--input', testPackageDir.path, '--link-to-remote'])); DartdocResults results = await dartdoc.generateDocs(); PackageGraph p = results.packageGraph; Package tuple = p.publicPackages.firstWhere((p) => p.name == 'tuple'); @@ -39,21 +43,27 @@ void main() { .properties .firstWhere((p) => p.name == 'useSomethingInAnotherPackage'); expect(tuple.documentedWhere, equals(DocumentLocation.remote)); + expect( + (useSomethingInAnotherPackage.modelType.typeArguments.first + as ParameterizedElementType) + .element + .package + .documentedWhere, + equals(DocumentLocation.remote)); expect( useSomethingInAnotherPackage.modelType.linkedName, startsWith( 'Tuple2')); - // Uncomment after SDK has appropriate configuration option added - // RegExp stringLink = new RegExp( - // 'https://api.dartlang.org/.*/${Platform.version.split(' ').first}/dart-core/String-class.html">String'); - // expect(useSomethingInAnotherPackage.modelType.linkedName, - // contains(stringLink)); + RegExp stringLink = new RegExp( + 'https://api.dartlang.org/(dev|stable|be)/${Platform.version.split(' ').first}/dart-core/String-class.html">String'); + expect(useSomethingInAnotherPackage.modelType.linkedName, + contains(stringLink)); }); test('generate docs for ${pathLib.basename(testPackageDir.path)} works', () async { Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv(['--input', testPackageDir.path]), tempDir); + await contextFromArgvTemp(['--input', testPackageDir.path])); DartdocResults results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -68,7 +78,7 @@ void main() { test('generate docs for ${pathLib.basename(testPackageBadDir.path)} fails', () async { Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv(['--input', testPackageBadDir.path]), tempDir); + await contextFromArgvTemp(['--input', testPackageBadDir.path])); try { await dartdoc.generateDocs(); @@ -80,8 +90,7 @@ void main() { test('generate docs for a package that does not have a readme', () async { Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv(['--input', testPackageWithNoReadme.path]), - tempDir); + await contextFromArgvTemp(['--input', testPackageWithNoReadme.path])); DartdocResults results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -94,10 +103,8 @@ void main() { }); test('generate docs including a single library', () async { - Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv( - ['--input', testPackageDir.path, '--include', 'fake']), - tempDir); + Dartdoc dartdoc = new Dartdoc.withoutGenerators(await contextFromArgvTemp( + ['--input', testPackageDir.path, '--include', 'fake'])); DartdocResults results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -110,10 +117,8 @@ void main() { }); test('generate docs excluding a single library', () async { - Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv( - ['--input', testPackageDir.path, '--exclude', 'fake']), - tempDir); + Dartdoc dartdoc = new Dartdoc.withoutGenerators(await contextFromArgvTemp( + ['--input', testPackageDir.path, '--exclude', 'fake'])); DartdocResults results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); @@ -129,9 +134,8 @@ void main() { test('generate docs for package with embedder yaml', () async { PackageMeta meta = new PackageMeta.fromDir(testPackageWithEmbedderYaml); if (meta.needsPubGet) meta.runPubGet(); - Dartdoc dartdoc = new Dartdoc.withoutGenerators( - await contextFromArgv(['--input', testPackageWithEmbedderYaml.path]), - tempDir); + Dartdoc dartdoc = new Dartdoc.withoutGenerators(await contextFromArgvTemp( + ['--input', testPackageWithEmbedderYaml.path])); DartdocResults results = await dartdoc.generateDocs(); expect(results.packageGraph, isNotNull); diff --git a/testing/test_package_flutter_plugin/.gitignore b/testing/test_package_flutter_plugin/.gitignore new file mode 100644 index 0000000000..33b0ba3c74 --- /dev/null +++ b/testing/test_package_flutter_plugin/.gitignore @@ -0,0 +1,3 @@ +ios/ +doc/api/ +pubspec.lock diff --git a/testing/test_package_flutter_plugin/analysis_options.yaml b/testing/test_package_flutter_plugin/analysis_options.yaml new file mode 100644 index 0000000000..4c1615ac5a --- /dev/null +++ b/testing/test_package_flutter_plugin/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + exclude: + - '**' diff --git a/testing/test_package_flutter_plugin/lib/testlib.dart b/testing/test_package_flutter_plugin/lib/testlib.dart new file mode 100644 index 0000000000..94cb275299 --- /dev/null +++ b/testing/test_package_flutter_plugin/lib/testlib.dart @@ -0,0 +1,12 @@ +/// This is a demonstration of interlinking with Flutter-using pub packages. +library testlib; + +import 'package:flutter/material.dart'; + +/// This widget is the best stateful widget ever. +class MyAwesomeWidget extends StatefulWidget { + MyAwesomeWidget({Key key}) : super(key: key) {} + + @override + State createState() => null; +} diff --git a/testing/test_package_flutter_plugin/pubspec.yaml b/testing/test_package_flutter_plugin/pubspec.yaml new file mode 100644 index 0000000000..3612d8e1cb --- /dev/null +++ b/testing/test_package_flutter_plugin/pubspec.yaml @@ -0,0 +1,12 @@ +name: test_package_flutter_plugin +version: 0.1.0 +description: A happy flutter almost plugin being documented +homepage: http://github.com/dart-lang/dartdoc/tree/master/testing/test_package_flutter_plugin + +dependencies: + flutter: + sdk: flutter + +environment: + sdk: ">2.0.0-dev.48.0 <3.0.0" + flutter: ">=0.1.4 <2.0.0" diff --git a/tool/grind.dart b/tool/grind.dart index b1813feec0..195c58c015 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -5,16 +5,39 @@ import 'dart:async'; import 'dart:io' hide ProcessException; +import 'package:dartdoc/src/dartdoc_options.dart'; import 'package:dartdoc/src/io_utils.dart'; import 'package:dartdoc/src/model_utils.dart'; import 'package:grinder/grinder.dart'; +import 'package:io/io.dart'; import 'package:path/path.dart' as pathLib; import 'package:yaml/yaml.dart' as yaml; main([List args]) => grind(args); -/// Run no more than 6 futures in parallel with this. -final MultiFutureTracker testFutures = new MultiFutureTracker(6); +/// Thrown on failure to find something in a file. +class GrindTestFailure { + final String message; + GrindTestFailure(this.message); +} + +/// Kind of an inefficient grepper for now. +expectFileContains(String path, List items) { + File source = new File(path); + if (!source.existsSync()) + throw new GrindTestFailure('file not found: ${path}'); + for (Pattern item in items) { + if (!new File(path).readAsStringSync().contains(item)) { + throw new GrindTestFailure('Can not find ${item} in ${path}'); + } + } +} + +/// Run no more than the number of processors available in parallel. +final MultiFutureTracker testFutures = new MultiFutureTracker( + Platform.environment.containsKey('TRAVIS') + ? 2 + : Platform.numberOfProcessors); // Directory.systemTemp is not a constant. So wrap it. Directory createTempSync(String prefix) => @@ -22,14 +45,72 @@ Directory createTempSync(String prefix) => final Memoizer tempdirsCache = new Memoizer(); +/// Global so that the lock is retained for the life of the process. +Future _lockFuture; +Completer _cleanFlutterRepo; + +/// Returns true if we need to replace the existing flutter. We never release +/// this lock until the program exits to prevent edge case runs from +/// spontaneously deciding to download a new Flutter SDK in the middle of a run. +Future get cleanFlutterRepo async { + if (_cleanFlutterRepo == null) { + // No await is allowed between check of _cleanFlutterRepo and its assignment, + // to prevent reentering this function. + _cleanFlutterRepo = new Completer(); + + // Figure out where the repository is supposed to be and lock updates for + // it. + await cleanFlutterDir.parent.create(recursive: true); + assert(_lockFuture == null); + _lockFuture = new File(pathLib.join(cleanFlutterDir.parent.path, 'lock')) + .openSync(mode: FileMode.WRITE) + .lock(); + await _lockFuture; + File lastSynced = + new File(pathLib.join(cleanFlutterDir.parent.path, 'lastSynced')); + FlutterRepo newRepo = + new FlutterRepo.fromPath(cleanFlutterDir.path, {}, 'clean'); + + // We have a repository, but is it up to date? + DateTime lastSyncedTime; + if (lastSynced.existsSync()) { + lastSyncedTime = new DateTime.fromMillisecondsSinceEpoch( + int.parse(lastSynced.readAsStringSync())); + } + if (lastSyncedTime == null || + new DateTime.now().difference(lastSyncedTime) > + new Duration(hours: 4)) { + // Rebuild the repository. + if (cleanFlutterDir.existsSync()) { + cleanFlutterDir.deleteSync(recursive: true); + } + cleanFlutterDir.createSync(recursive: true); + await newRepo._init(); + await lastSynced.writeAsString( + (new DateTime.now()).millisecondsSinceEpoch.toString()); + } + _cleanFlutterRepo.complete(newRepo); + } + return _cleanFlutterRepo.future; +} + Directory get dartdocDocsDir => tempdirsCache.memoized1(createTempSync, 'dartdoc'); +Directory get dartdocDocsDirRemote => + tempdirsCache.memoized1(createTempSync, 'dartdoc_remote'); Directory get sdkDocsDir => tempdirsCache.memoized1(createTempSync, 'sdkdocs'); +Directory cleanFlutterDir = new Directory( + pathLib.join(resolveTildePath('~/.dartdoc_grinder'), 'cleanFlutter')); Directory get flutterDir => tempdirsCache.memoized1(createTempSync, 'flutter'); Directory get testPackage => new Directory(pathLib.joinAll(['testing', 'test_package'])); +Directory get pluginPackage => + new Directory(pathLib.joinAll(['testing', 'test_package_flutter_plugin'])); + Directory get testPackageDocsDir => tempdirsCache.memoized1(createTempSync, 'test_package'); +Directory get pluginPackageDocsDir => + tempdirsCache.memoized1(createTempSync, 'test_package_flutter_plugin'); /// Version of dartdoc we should use when making comparisons. String get dartdocOriginalBranch { @@ -194,6 +275,7 @@ WarningsCollection jsonMessageIterableToWarnings(Iterable messageIterable, String tempPath, String pubDir, String branch) { WarningsCollection warningTexts = new WarningsCollection(tempPath, pubDir, branch); + if (messageIterable == null) return warningTexts; for (Map message in messageIterable) { if (message.containsKey('level') && message['level'] == 'WARNING' && @@ -341,9 +423,12 @@ Future compareFlutterWarnings() async { Map envCurrent = _createThrowawayPubCache(); Map envOriginal = _createThrowawayPubCache(); Future currentDartdocFlutterBuild = _buildFlutterDocs(flutterDir.path, - new Future.value(Directory.current.path), envCurrent, 'current'); + new Future.value(Directory.current.path), envCurrent, 'docs-current'); Future originalDartdocFlutterBuild = _buildFlutterDocs( - originalDartdocFlutter.path, originalDartdoc, envOriginal, 'original'); + originalDartdocFlutter.path, + originalDartdoc, + envOriginal, + 'docs-original'); WarningsCollection currentDartdocWarnings = jsonMessageIterableToWarnings( await currentDartdocFlutterBuild, flutterDir.absolute.path, @@ -397,52 +482,100 @@ Future serveFlutterDocs() async { ]); } +@Task('Validate flutter docs') +@Depends(testDartdocFlutterPlugin, buildFlutterDocs) +validateFlutterDocs() {} + @Task('Build flutter docs') Future buildFlutterDocs() async { log('building flutter docs into: $flutterDir'); Map env = _createThrowawayPubCache(); await _buildFlutterDocs( - flutterDir.path, new Future.value(Directory.current.path), env); + flutterDir.path, new Future.value(Directory.current.path), env, 'docs'); String index = new File( pathLib.join(flutterDir.path, 'dev', 'docs', 'doc', 'index.html')) .readAsStringSync(); stdout.write(index); } +/// A class wrapping a flutter SDK. +class FlutterRepo { + final String flutterPath; + final Map env; + final String bin = pathLib.join('bin', 'flutter'); + + FlutterRepo._(this.flutterPath, this.env, String label) { + cacheDart = + pathLib.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'dart'); + cachePub = + pathLib.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'pub'); + env['PATH'] = + '${pathLib.join(pathLib.canonicalize(flutterPath), "bin")}:${env['PATH'] ?? Platform.environment['PATH']}'; + env['FLUTTER_ROOT'] = flutterPath; + launcher = + new SubprocessLauncher('flutter${label == null ? "" : "-$label"}', env); + } + + Future _init() async { + new Directory(flutterPath).createSync(recursive: true); + await launcher.runStreamed( + 'git', ['clone', 'https://github.com/flutter/flutter.git', '.'], + workingDirectory: flutterPath); + await launcher.runStreamed( + bin, + ['--version'], + workingDirectory: flutterPath, + ); + await launcher.runStreamed( + bin, + ['precache'], + workingDirectory: flutterPath, + ); + } + + factory FlutterRepo.fromPath(String flutterPath, Map env, + [String label]) { + FlutterRepo flutterRepo = new FlutterRepo._(flutterPath, env, label); + return flutterRepo; + } + + /// Copy an existing, initialized flutter repo. + static Future copyFromExistingFlutterRepo( + FlutterRepo origRepo, String flutterPath, Map env, + [String label]) async { + await copyPath(origRepo.flutterPath, flutterPath); + FlutterRepo flutterRepo = new FlutterRepo._(flutterPath, env, label); + return flutterRepo; + } + + /// Doesn't actually copy the existing repo; use for read-only operations only. + static Future fromExistingFlutterRepo(FlutterRepo origRepo, + [String label]) async { + FlutterRepo flutterRepo = + new FlutterRepo._(origRepo.flutterPath, {}, label); + return flutterRepo; + } + + String cacheDart; + String cachePub; + SubprocessLauncher launcher; +} + Future> _buildFlutterDocs( String flutterPath, Future futureCwd, Map env, [String label]) async { - env['PATH'] = '${pathLib.join(flutterPath, "bin")}:${env['PATH']}'; - var launcher = new SubprocessLauncher( - 'build-flutter-docs${label == null ? "" : "-$label"}', env); - await launcher.runStreamed( - 'git', ['clone', 'https://github.com/flutter/flutter.git', '.'], - workingDirectory: flutterPath); - String flutterBin = pathLib.join('bin', 'flutter'); - String flutterCacheDart = - pathLib.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'dart'); - String flutterCachePub = - pathLib.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'pub'); - await launcher.runStreamed( - flutterBin, - ['--version'], - workingDirectory: flutterPath, - ); - await launcher.runStreamed( - flutterBin, - ['precache'], - workingDirectory: flutterPath, - ); - await launcher.runStreamed( - flutterCachePub, + FlutterRepo flutterRepo = await FlutterRepo.copyFromExistingFlutterRepo( + await cleanFlutterRepo, flutterPath, env, label); + await flutterRepo.launcher.runStreamed( + flutterRepo.cachePub, ['get'], workingDirectory: pathLib.join(flutterPath, 'dev', 'tools'), ); - await launcher.runStreamed( - flutterCachePub, ['global', 'activate', '-spath', '.'], + await flutterRepo.launcher.runStreamed( + flutterRepo.cachePub, ['global', 'activate', '-spath', '.'], workingDirectory: await futureCwd); - return await launcher.runStreamed( - flutterCacheDart, + return await flutterRepo.launcher.runStreamed( + flutterRepo.cacheDart, [pathLib.join('dev', 'tools', 'dartdoc.dart'), '-c', '--json'], workingDirectory: flutterPath, ); @@ -613,6 +746,7 @@ testPreviewDart2() async { .where((f) => !f.path.endsWith('html_generator_test.dart') && !Platform.isWindows) .where((f) => + // grinder stopped working with preview-dart-2. !f.path.endsWith('grind_test.dart'))) { // absolute path to work around dart-lang/sdk#32901 await testFutures.addFuture(new SubprocessLauncher( @@ -627,7 +761,6 @@ testPreviewDart2() async { testDart1() async { List parameters = ['--checked']; - for (File dartFile in testFiles) { // absolute path to work around dart-lang/sdk#32901 await testFutures.addFuture(new SubprocessLauncher( @@ -645,8 +778,69 @@ testDartdoc() async { var launcher = new SubprocessLauncher('test-dartdoc'); await launcher.runStreamed(Platform.resolvedExecutable, ['--checked', 'bin/dartdoc.dart', '--output', dartdocDocsDir.path]); - File indexHtml = joinFile(dartdocDocsDir, ['index.html']); - if (!indexHtml.existsSync()) fail('docs not generated'); + expectFileContains(pathLib.join(dartdocDocsDir.path, 'index.html'), + ['dartdoc - Dart API docs']); + final RegExp object = new RegExp('
  • Object
  • ', multiLine: true); + expectFileContains( + pathLib.join(dartdocDocsDir.path, 'dartdoc', 'ModelElement-class.html'), + [object]); +} + +@Task('Generate docs for dartdoc with remote linking') +testDartdocRemote() async { + var launcher = new SubprocessLauncher('test-dartdoc-remote'); + final RegExp object = new RegExp( + 'Object', + multiLine: true); + await launcher.runStreamed(Platform.resolvedExecutable, [ + '--checked', + 'bin/dartdoc.dart', + '--link-to-remote', + '--output', + dartdocDocsDir.path + ]); + expectFileContains(pathLib.join(dartdocDocsDir.path, 'index.html'), + ['dartdoc - Dart API docs']); + expectFileContains( + pathLib.join(dartdocDocsDir.path, 'dartdoc', 'ModelElement-class.html'), + [object]); +} + +@Task('serve docs for a package that requires flutter with remote linking') +@Depends(buildDartdocFlutterPluginDocs) +Future serveDartdocFlutterPluginDocs() async { + await _serveDocsFrom( + pluginPackageDocsDir.path, 8005, 'serve-dartdoc-flutter-plugin-docs'); +} + +@Task('Build docs for a package that requires flutter with remote linking') +buildDartdocFlutterPluginDocs() async { + FlutterRepo flutterRepo = await FlutterRepo.fromExistingFlutterRepo( + await cleanFlutterRepo, 'docs-flutter-plugin'); + + await flutterRepo.launcher.runStreamed( + Platform.resolvedExecutable, + [ + '--checked', + pathLib.join(Directory.current.path, 'bin', 'dartdoc.dart'), + '--link-to-remote', + '--output', + pluginPackageDocsDir.path + ], + workingDirectory: pluginPackage.path); +} + +@Task('Verify docs for a package that requires flutter with remote linking') +@Depends(buildDartdocFlutterPluginDocs) +testDartdocFlutterPlugin() async { + // Verify that links to Dart SDK and Flutter SDK go to the flutter site. + expectFileContains( + pathLib.join( + pluginPackageDocsDir.path, 'testlib', 'MyAwesomeWidget-class.html'), + [ + 'Widget', + 'Object' + ]); } @Task('update test_package_docs') diff --git a/tool/travis.sh b/tool/travis.sh index ec4731f3b9..6f4cceb121 100755 --- a/tool/travis.sh +++ b/tool/travis.sh @@ -15,26 +15,12 @@ if [ "$DARTDOC_BOT" = "sdk-docs" ]; then # silence stdout but echo stderr echo "" echo "Building and validating SDK docs..." - pub run grinder validate-sdk-docs - echo "SDK docs process finished" elif [ "$DARTDOC_BOT" = "flutter" ]; then echo "Running flutter dartdoc bot" - - pub run grinder build-flutter-docs + pub run grinder validate-flutter-docs else echo "Running main dartdoc bot" - - # Verify that the libraries are error free. - pub run grinder analyze - - # Run dartdoc on test_package. - (cd testing/test_package; dart -c ../../bin/dartdoc.dart) - - # And on test_package_small. - (cd testing/test_package_small; dart -c ../../bin/dartdoc.dart) - - # Run the tests. - pub run test + pub run grinder buildbot fi