diff --git a/.github/workflows/nnbd-test.yaml b/.github/workflows/nnbd-test.yaml new file mode 100644 index 0000000000..fdeaaaf0e0 --- /dev/null +++ b/.github/workflows/nnbd-test.yaml @@ -0,0 +1,70 @@ +name: Test + +on: + # Run CI on pushes to the main branch, and on PRs against main. + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: 0 15 * * * + +env: + PUB_ENVIRONMENT: bot.github + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + sdk: [dev, stable] + job: [main, flutter, sdk-analyzer, packages, sdk-docs] + include: + - os: macos-latest + sdk: dev + job: main + - os: windows-latest + sdk: dev + job: main + exclude: + # Do not try to run flutter against the "stable" sdk, + # it is unlikely to work and produces uninteresting + # results. + - sdk: stable + job: flutter + - sdk: stable + job: sdk-analyzer + + steps: + - name: Configure git + if: runner.os == 'Windows' + run: git config --global core.autocrlf input + - uses: actions/checkout@v2 + - uses: dart-lang/setup-dart@v1.0 + with: + sdk: ${{ matrix.sdk }} + - name: Install dependencies + run: dart pub get + - name: ${{ matrix.job }} + if: runner.os != 'Windows' + run: ./tool/travis.sh + env: + DARTDOC_BOT: ${{ matrix.job }} + #COVERAGE_TOKEN: true # this needs to be set to enable coverage + - name: ${{ matrix.job }} + if: runner.os == 'Windows' && matrix.job == 'main' + run: dart run grinder buildbot + env: + DARTDOC_BOT: ${{ matrix.job }} + # - id: coverage + # name: Upload coverage + # if: runner.os == 'Linux' && matrix.job == 'main' && matrix.sdk == 'dev' + # uses: coverallsapp/github-action@v1.1.2 + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} + # path-to-lcov: lcov.info + # - name: Echo coveralls api result + # if: runner.os == 'Linux' && matrix.job == 'main' && matrix.sdk == 'dev' + # run: echo ${{ steps.coverage.outputs['coveralls-api-result'] }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 474d4345f8..58fe76358c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -1,11 +1,11 @@ -name: Test +name: Test - NNBD branch on: # Run CI on pushes to the main branch, and on PRs against main. push: - branches: [ master ] + branches: [ nnbd ] pull_request: - branches: [ master ] + branches: [ nnbd ] schedule: - cron: 0 15 * * * @@ -20,14 +20,14 @@ jobs: matrix: os: [ubuntu-latest] sdk: [dev, stable] - job: [main, flutter, sdk-analyzer, packages, sdk-docs] + job: [nnbd, flutter, sdk-analyzer, packages, sdk-docs] include: - os: macos-latest sdk: dev - job: main + job: nnbd - os: windows-latest sdk: dev - job: main + job: nnbd exclude: # Do not try to run flutter against the "stable" sdk, # it is unlikely to work and produces uninteresting @@ -76,8 +76,8 @@ jobs: DARTDOC_BOT: ${{ matrix.job }} #COVERAGE_TOKEN: true # this needs to be set to enable coverage - name: ${{ matrix.job }} - if: runner.os == 'Windows' && matrix.job == 'main' - run: dart run grinder buildbot + if: runner.os == 'Windows' && matrix.job == 'nnbd' + run: dart run grinder buildbot-no-publish env: DARTDOC_BOT: ${{ matrix.job }} # - id: coverage diff --git a/analysis_options.yaml b/analysis_options.yaml index 3096bb152d..76c1fdcda5 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -14,12 +14,27 @@ analyzer: - 'lib/src/third_party/pkg/**' - 'lib/templates/*.html' - 'pub.dartlang.org/**' - - 'testing/**' + # These have a lot of lint. + - 'testing/test_package/**' + - 'testing/test_package_custom_templates/**' + - 'testing/test_package_experiments/**' + - 'testing/test_package_export_error/**' + - 'testing/test_package_extensions/**' + - 'testing/test_package_import_export_error/**' + - 'testing/test_package_imported/**' + - 'testing/test_package_options/**' + # This package imports flutter, which is perhaps not found by the base `dart analyze` tool. - 'testing/flutter_packages/test_package_flutter_plugin/**' + # This package is meant to mock the sky_engine package. + - 'testing/sky_engine/**' + # These packages have compile-time errors. + - 'testing/test_package_bad/**' - 'testing/test_package_export_error/**' linter: rules: always_declare_return_types: true + always_put_required_named_parameters_first: true + avoid_bool_literals_in_conditional_expressions: true avoid_dynamic_calls: true avoid_single_cascade_in_expression_statements: true avoid_unused_constructor_parameters: true @@ -29,8 +44,10 @@ linter: package_api_docs: true prefer_final_fields: true prefer_initializing_formals: true + prefer_single_quotes: true prefer_void_to_null: true slash_for_doc_comments: true type_annotate_public_apis: true # Work in progress canonical score lints unawaited_futures: true + unnecessary_null_aware_assignments: true diff --git a/analysis_options_presubmit.yaml b/analysis_options_presubmit.yaml index df4b78d7e4..ece55a3a9b 100644 --- a/analysis_options_presubmit.yaml +++ b/analysis_options_presubmit.yaml @@ -10,6 +10,8 @@ analyzer: ### Extra ignores for presubmit deprecated_member_use: ignore deprecated_member_use_from_same_package: ignore + ### Temporary presubmit ignore for NNBD migration + import_of_legacy_library_into_null_safe: ignore language: strict-raw-types: true exclude: @@ -17,12 +19,26 @@ analyzer: - 'lib/src/third_party/pkg/**' - 'lib/templates/*.html' - 'pub.dartlang.org/**' - - 'testing/**' - - 'testing/flutter_packages/test_package_flutter_plugin/**' + # These have a lot of lint. + - 'testing/test_package/**' + - 'testing/test_package_custom_templates/**' + - 'testing/test_package_experiments/**' - 'testing/test_package_export_error/**' + - 'testing/test_package_extensions/**' + - 'testing/test_package_import_export_error/**' + - 'testing/test_package_imported/**' + - 'testing/test_package_options/**' + # This package imports flutter, which is perhaps not found by the base `dart analyze` tool. + - 'testing/flutter_packages/test_package_flutter_plugin/**' + # This package is meant to mock the sky_engine package. + - 'testing/sky_engine/**' + # These packages have compile-time errors. + - 'testing/test_package_bad/**' linter: rules: always_declare_return_types: true + always_put_required_named_parameters_first: true + avoid_bool_literals_in_conditional_expressions: true avoid_dynamic_calls: true avoid_single_cascade_in_expression_statements: true avoid_unused_constructor_parameters: true @@ -32,8 +48,10 @@ linter: package_api_docs: true prefer_final_fields: true prefer_initializing_formals: true + prefer_single_quotes: true prefer_void_to_null: true slash_for_doc_comments: true type_annotate_public_apis: true # Work in progress canonical score lints unawaited_futures: true + unnecessary_null_aware_assignments: true diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart index f9ae1ec22c..00564432aa 100644 --- a/lib/dartdoc.dart +++ b/lib/dartdoc.dart @@ -8,515 +8,12 @@ @experimental library dartdoc; -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' show Platform, exitCode, stderr; - -import 'package:analyzer/file_system/file_system.dart'; -import 'package:dartdoc/options.dart'; -import 'package:dartdoc/src/dartdoc_options.dart'; -import 'package:dartdoc/src/failure.dart'; -import 'package:dartdoc/src/generator/empty_generator.dart'; -import 'package:dartdoc/src/generator/generator.dart'; -import 'package:dartdoc/src/generator/html_generator.dart'; -import 'package:dartdoc/src/generator/markdown_generator.dart'; -import 'package:dartdoc/src/logging.dart'; -import 'package:dartdoc/src/matching_link_result.dart'; -import 'package:dartdoc/src/model/model.dart'; -import 'package:dartdoc/src/package_meta.dart'; -import 'package:dartdoc/src/tuple.dart'; -import 'package:dartdoc/src/utils.dart'; -import 'package:dartdoc/src/version.dart'; -import 'package:dartdoc/src/warnings.dart'; -import 'package:html/parser.dart' show parse; import 'package:meta/meta.dart'; -import 'package:path/path.dart' as path; +export 'package:dartdoc/src/dartdoc.dart'; export 'package:dartdoc/src/dartdoc_options.dart'; export 'package:dartdoc/src/element_type.dart'; export 'package:dartdoc/src/generator/generator.dart'; export 'package:dartdoc/src/model/model.dart'; export 'package:dartdoc/src/package_config_provider.dart'; export 'package:dartdoc/src/package_meta.dart'; - -const String programName = 'dartdoc'; -// Update when pubspec version changes by running `pub run build_runner build` -const String dartdocVersion = packageVersion; - -class DartdocFileWriter implements FileWriter { - final String _outputDir; - @override - final ResourceProvider resourceProvider; - final Map _fileElementMap = {}; - @override - final Set writtenFiles = {}; - - DartdocFileWriter(this._outputDir, this.resourceProvider); - - @override - void writeBytes( - String filePath, - List content, { - bool allowOverwrite = false, - }) { - // Replace '/' separators with proper separators for the platform. - var outFile = path.joinAll(filePath.split('/')); - - if (!allowOverwrite) { - _warnAboutOverwrite(outFile, null); - } - _fileElementMap[outFile] = null; - - var file = _getFile(outFile); - file.writeAsBytesSync(content); - writtenFiles.add(outFile); - logProgress(outFile); - } - - @override - void write(String filePath, String content, {Warnable element}) { - // Replace '/' separators with proper separators for the platform. - var outFile = path.joinAll(filePath.split('/')); - - _warnAboutOverwrite(outFile, element); - _fileElementMap[outFile] = element; - - var file = _getFile(outFile); - file.writeAsStringSync(content); - writtenFiles.add(outFile); - logProgress(outFile); - } - - void _warnAboutOverwrite(String outFile, Warnable element) { - if (_fileElementMap.containsKey(outFile)) { - assert(element != null, - 'Attempted overwrite of $outFile without corresponding element'); - var originalElement = _fileElementMap[outFile]; - var referredFrom = originalElement != null ? [originalElement] : null; - element?.warn(PackageWarning.duplicateFile, - message: outFile, referredFrom: referredFrom); - } - } - - /// Returns the file at [outFile] relative to [_outputDir], creating the - /// parent directory if necessary. - File _getFile(String outFile) { - var file = resourceProvider - .getFile(resourceProvider.pathContext.join(_outputDir, outFile)); - var parent = file.parent2; - if (!parent.exists) { - parent.create(); - } - return file; - } -} - -/// Generates Dart documentation for all public Dart libraries in the given -/// directory. -class Dartdoc { - Generator _generator; - final PackageBuilder packageBuilder; - final DartdocOptionContext config; - final Set _writtenFiles = {}; - Folder _outputDir; - - // Fires when the self checks make progress. - final StreamController _onCheckProgress = - StreamController(sync: true); - - Dartdoc._(this.config, this._generator, this.packageBuilder) { - _outputDir = config.resourceProvider - .getFolder(config.resourceProvider.pathContext.absolute(config.output)) - ..create(); - } - - // TODO(srawlins): Remove when https://github.com/dart-lang/linter/issues/2706 - // is fixed. - // ignore: unnecessary_getters_setters - Generator get generator => _generator; - - @visibleForTesting - // TODO(srawlins): Remove when https://github.com/dart-lang/linter/issues/2706 - // is fixed. - // ignore: unnecessary_getters_setters - set generator(Generator newGenerator) => _generator = newGenerator; - - /// Asynchronous factory method that builds Dartdoc with an empty generator. - static Future withEmptyGenerator( - DartdocOptionContext config, - PackageBuilder packageBuilder, - ) async { - return Dartdoc._( - config, - await initEmptyGenerator(config), - packageBuilder, - ); - } - - /// Asynchronous factory method that builds Dartdoc with a generator - /// determined by the given context. - static Future fromContext( - DartdocGeneratorOptionContext context, - PackageBuilder packageBuilder, - ) async { - Generator generator; - switch (context.format) { - case 'html': - generator = await initHtmlGenerator(context); - break; - case 'md': - generator = await initMarkdownGenerator(context); - break; - default: - throw DartdocFailure('Unsupported output format: ${context.format}'); - } - return Dartdoc._( - context, - generator, - packageBuilder, - ); - } - - Stream get onCheckProgress => _onCheckProgress.stream; - - @visibleForTesting - Future generateDocsBase() async { - var stopwatch = Stopwatch()..start(); - var packageGraph = await packageBuilder.buildPackageGraph(); - var seconds = stopwatch.elapsedMilliseconds / 1000.0; - var libs = packageGraph.libraries.length; - logInfo("Initialized dartdoc with $libs librar${libs == 1 ? 'y' : 'ies'} " - 'in ${seconds.toStringAsFixed(1)} seconds'); - stopwatch.reset(); - - // Create the out directory. - if (!_outputDir.exists) _outputDir.create(); - - var writer = DartdocFileWriter(_outputDir.path, config.resourceProvider); - await generator.generate(packageGraph, writer); - - _writtenFiles.addAll(writer.writtenFiles); - if (config.validateLinks && _writtenFiles.isNotEmpty) { - _validateLinks(packageGraph, _outputDir.path); - } - - var warnings = packageGraph.packageWarningCounter.warningCount; - var errors = packageGraph.packageWarningCounter.errorCount; - if (warnings == 0 && errors == 0) { - logInfo('no issues found'); - } else { - logWarning("Found $warnings ${pluralize('warning', warnings)} " - "and $errors ${pluralize('error', errors)}."); - } - - seconds = stopwatch.elapsedMilliseconds / 1000.0; - libs = packageGraph.localPublicLibraries.length; - logInfo("Documented $libs public librar${libs == 1 ? 'y' : 'ies'} " - 'in ${seconds.toStringAsFixed(1)} seconds'); - - if (config.showStats) { - logInfo(markdownStats.buildReport()); - } - return DartdocResults(config.topLevelPackageMeta, packageGraph, _outputDir); - } - - /// Generate Dartdoc documentation. - /// - /// [DartdocResults] is returned if dartdoc succeeds. [DartdocFailure] is - /// thrown if dartdoc fails in an expected way, for example if there is an - /// analysis error in the code. - Future generateDocs() async { - DartdocResults dartdocResults; - try { - logInfo('Documenting ${config.topLevelPackageMeta}...'); - - dartdocResults = await generateDocsBase(); - if (dartdocResults.packageGraph.localPublicLibraries.isEmpty) { - logWarning('dartdoc could not find any libraries to document'); - } - - final errorCount = - dartdocResults.packageGraph.packageWarningCounter.errorCount; - if (errorCount > 0) { - throw DartdocFailure('encountered $errorCount errors'); - } - var outDirPath = config.resourceProvider.pathContext - .absolute(dartdocResults.outDir.path); - logInfo('Success! Docs generated into $outDirPath'); - return dartdocResults; - } finally { - dartdocResults?.packageGraph?.dispose(); - } - } - - /// Warn on file paths. - void _warn(PackageGraph packageGraph, PackageWarning kind, String warnOn, - String origin, - {String referredFrom}) { - // Ordinarily this would go in [Package.warn], but we don't actually know what - // ModelElement to warn on yet. - Warnable warnOnElement; - var referredFromElements = {}; - Set warnOnElements; - - // Make all paths relative to origin. - if (path.isWithin(origin, warnOn)) { - warnOn = path.relative(warnOn, from: origin); - } - if (referredFrom != null) { - if (path.isWithin(origin, referredFrom)) { - referredFrom = path.relative(referredFrom, from: origin); - } - // Source paths are always relative. - if (_hrefs[referredFrom] != null) { - referredFromElements.addAll(_hrefs[referredFrom]); - } - } - warnOnElements = _hrefs[warnOn]; - - if (referredFromElements.any((e) => e.isCanonical)) { - referredFromElements.removeWhere((e) => !e.isCanonical); - } - if (warnOnElements != null) { - warnOnElement = warnOnElements.firstWhere((e) => e.isCanonical, - orElse: () => warnOnElements.isEmpty ? null : warnOnElements.first); - } - - if (referredFromElements.isEmpty && referredFrom == 'index.html') { - referredFromElements.add(packageGraph.defaultPackage); - } - var message = warnOn; - if (referredFrom == 'index.json') message = '$warnOn (from index.json)'; - packageGraph.warnOnElement(warnOnElement, kind, - message: message, referredFrom: referredFromElements); - } - - void _doOrphanCheck( - PackageGraph packageGraph, String origin, Set visited) { - var normalOrigin = path.normalize(origin); - var staticAssets = path.joinAll([normalOrigin, 'static-assets', '']); - var indexJson = path.joinAll([normalOrigin, 'index.json']); - var foundIndexJson = false; - - void checkDirectory(Folder dir) { - for (var f in dir.getChildren()) { - if (f is Folder) { - checkDirectory(f); - continue; - } - var fullPath = path.normalize(f.path); - if (fullPath.startsWith(staticAssets)) { - continue; - } - if (path.equals(fullPath, indexJson)) { - foundIndexJson = true; - _onCheckProgress.add(fullPath); - continue; - } - if (visited.contains(fullPath)) continue; - var relativeFullPath = path.relative(fullPath, from: normalOrigin); - if (!_writtenFiles.contains(relativeFullPath)) { - // This isn't a file we wrote (this time); don't claim we did. - _warn( - packageGraph, PackageWarning.unknownFile, fullPath, normalOrigin); - } else { - // Error messages are orphaned by design and do not appear in the search - // index. - if (const {'__404error.html', 'categories.json'}.contains(fullPath)) { - _warn(packageGraph, PackageWarning.orphanedFile, fullPath, - normalOrigin); - } - } - _onCheckProgress.add(fullPath); - } - } - - checkDirectory(config.resourceProvider.getFolder(normalOrigin)); - - if (!foundIndexJson) { - _warn(packageGraph, PackageWarning.brokenLink, indexJson, normalOrigin); - _onCheckProgress.add(indexJson); - } - } - - // This is extracted to save memory during the check; be careful not to hang - // on to anything referencing the full file and doc tree. - Tuple2, String> _getStringLinksAndHref(String fullPath) { - var file = config.resourceProvider.getFile(fullPath); - if (!file.exists) { - return null; - } - // TODO(srawlins): It is possible that instantiating an HtmlParser using - // `lowercaseElementName: false` and `lowercaseAttrName: false` may save - // time or memory. - var doc = parse(file.readAsBytesSync()); - var base = doc.querySelector('base'); - String baseHref; - if (base != null) { - baseHref = base.attributes['href']; - } - var links = doc.querySelectorAll('a'); - var stringLinks = links - .map((link) => link.attributes['href']) - .where((href) => href != null && href != '') - .toList(); - - return Tuple2(stringLinks, baseHref); - } - - void _doSearchIndexCheck( - PackageGraph packageGraph, String origin, Set visited) { - var fullPath = path.joinAll([origin, 'index.json']); - var indexPath = path.joinAll([origin, 'index.html']); - var file = config.resourceProvider.getFile(fullPath); - if (!file.exists) { - return; - } - var decoder = JsonDecoder(); - List jsonData = decoder.convert(file.readAsStringSync()); - - var found = {}; - found.add(fullPath); - // The package index isn't supposed to be in the search, so suppress the - // warning. - found.add(indexPath); - for (Map entry in jsonData) { - if (entry.containsKey('href')) { - var entryPath = - path.joinAll([origin, ...path.posix.split(entry['href'])]); - if (!visited.contains(entryPath)) { - _warn(packageGraph, PackageWarning.brokenLink, entryPath, - path.normalize(origin), - referredFrom: fullPath); - } - found.add(entryPath); - } - } - // Missing from search index - var missingFromSearch = visited.difference(found); - for (var s in missingFromSearch) { - _warn(packageGraph, PackageWarning.missingFromSearchIndex, s, - path.normalize(origin), - referredFrom: fullPath); - } - } - - void _doCheck(PackageGraph packageGraph, String origin, Set visited, - String pathToCheck, - [String source, String fullPath]) { - fullPath ??= path.normalize(path.joinAll([origin, pathToCheck])); - - var stringLinksAndHref = _getStringLinksAndHref(fullPath); - if (stringLinksAndHref == null) { - _warn(packageGraph, PackageWarning.brokenLink, pathToCheck, - path.normalize(origin), - referredFrom: source); - _onCheckProgress.add(pathToCheck); - // Remove so that we properly count that the file doesn't exist for - // the orphan check. - visited.remove(fullPath); - return; - } - visited.add(fullPath); - var stringLinks = stringLinksAndHref.item1; - var baseHref = stringLinksAndHref.item2; - - // Prevent extremely large stacks by storing the paths we are using - // here instead -- occasionally, very large jobs have overflowed - // the stack without this. - // (newPathToCheck, newFullPath) - var toVisit = >{}; - - final ignoreHyperlinks = RegExp(r'^(https:|http:|mailto:|ftp:)'); - for (final href in stringLinks) { - if (!href.startsWith(ignoreHyperlinks)) { - final uri = Uri.tryParse(href); - - if (uri == null || !uri.hasAuthority && !uri.hasFragment) { - String full; - if (baseHref != null) { - full = '${path.dirname(pathToCheck)}/$baseHref/$href'; - } else { - full = '${path.dirname(pathToCheck)}/$href'; - } - - final newPathToCheck = path.normalize(full); - final newFullPath = - path.normalize(path.joinAll([origin, newPathToCheck])); - if (!visited.contains(newFullPath)) { - toVisit.add(Tuple2(newPathToCheck, newFullPath)); - visited.add(newFullPath); - } - } - } - } - for (var visitPaths in toVisit) { - _doCheck(packageGraph, origin, visited, visitPaths.item1, pathToCheck, - visitPaths.item2); - } - _onCheckProgress.add(pathToCheck); - } - - Map> _hrefs; - - /// Don't call this method more than once, and only after you've - /// generated all docs for the Package. - void _validateLinks(PackageGraph packageGraph, String origin) { - assert(_hrefs == null); - _hrefs = packageGraph.allHrefs; - - final visited = {}; - logInfo('Validating docs...'); - _doCheck(packageGraph, origin, visited, 'index.html'); - _doOrphanCheck(packageGraph, origin, visited); - _doSearchIndexCheck(packageGraph, origin, visited); - } - - /// Runs [generateDocs] function and properly handles the errors. - /// - /// Passing in a [postProcessCallback] to do additional processing after - /// the documentation is generated. - void executeGuarded([ - Future Function(DartdocOptionContext) postProcessCallback, - ]) { - onCheckProgress.listen(logProgress); - // This function should *never* await `runZonedGuarded` because the errors - // thrown in generateDocs are uncaught. We want this because uncaught errors - // cause IDE debugger to automatically stop at the exception. - // - // If you await the zone, the code that comes after the await is not - // executed if the zone dies due to uncaught error. To avoid this confusion, - // never await `runZonedGuarded` and never change the return value of - // [executeGuarded]. - runZonedGuarded( - () async { - await generateDocs(); - await postProcessCallback?.call(config); - }, - (e, chain) { - if (e is DartdocFailure) { - stderr.writeln('\n$_dartdocFailedMessage: $e.'); - exitCode = 1; - } else { - stderr.writeln('\n$_dartdocFailedMessage: $e\n$chain'); - exitCode = 255; - } - }, - zoneSpecification: ZoneSpecification( - print: (_, __, ___, String line) => logPrint(line), - ), - ); - } -} - -/// The results of a [Dartdoc.generateDocs] call. -class DartdocResults { - final PackageMeta packageMeta; - final PackageGraph packageGraph; - final Folder outDir; - - DartdocResults(this.packageMeta, this.packageGraph, this.outDir); -} - -String get _dartdocFailedMessage => - 'dartdoc $packageVersion (${Platform.script.path}) failed'; diff --git a/lib/options.dart b/lib/options.dart index e4e904e70a..01c3f60e8c 100644 --- a/lib/options.dart +++ b/lib/options.dart @@ -13,31 +13,21 @@ class DartdocGeneratorOptionContext extends DartdocOptionContext { DartdocGeneratorOptionContext( DartdocOptionSet optionSet, Folder dir, ResourceProvider resourceProvider) : super(optionSet, dir, resourceProvider); - DartdocGeneratorOptionContext.fromDefaultContextLocation( DartdocOptionSet optionSet, ResourceProvider resourceProvider) : super.fromDefaultContextLocation(optionSet, resourceProvider); - // TODO(migration): Make late final with initializer when Null Safe. - String _header; - /// Returns the joined contents of any 'header' files specified in options. - String get header => - _header ??= _joinCustomTextFiles(optionSet['header'].valueAt(context)); - - // TODO(migration): Make late final with initializer when Null Safe. - String _footer; + late final String header = + _joinCustomTextFiles(optionSet['header'].valueAt(context)); /// Returns the joined contents of any 'footer' files specified in options. - String get footer => - _footer ??= _joinCustomTextFiles(optionSet['footer'].valueAt(context)); - - // TODO(migration): Make late final with initializer when Null Safe. - String _footerText; + late final String footer = + _joinCustomTextFiles(optionSet['footer'].valueAt(context)); /// Returns the joined contents of any 'footer-text' files specified in /// options. - String get footerText => _footerText ??= + late final String footerText = _joinCustomTextFiles(optionSet['footerText'].valueAt(context)); String _joinCustomTextFiles(Iterable paths) => paths @@ -46,20 +36,20 @@ class DartdocGeneratorOptionContext extends DartdocOptionContext { bool get prettyIndexJson => optionSet['prettyIndexJson'].valueAt(context); - String get favicon => optionSet['favicon'].valueAt(context); + String? get favicon => optionSet['favicon'].valueAt(context); - String get relCanonicalPrefix => + String? get relCanonicalPrefix => optionSet['relCanonicalPrefix'].valueAt(context); /// The 'templatesDir' dartdoc option if one was specified; otherwise `null`. - String get templatesDir => optionSet['templatesDir'].valueAt(context); + String? get templatesDir => optionSet['templatesDir'].valueAt(context); // TODO(jdkoren): duplicated temporarily so that GeneratorContext is enough for configuration. @override bool get useBaseHref => optionSet['useBaseHref'].valueAt(context); /// The 'resourcesDir' dartdoc option if one was specified; otherwise `null`. - String /*?*/ get resourcesDir => optionSet['resourcesDir'].valueAt(context); + String? get resourcesDir => optionSet['resourcesDir'].valueAt(context); } class DartdocProgramOptionContext extends DartdocGeneratorOptionContext @@ -92,12 +82,12 @@ Future>> createDartdocProgramOptions( ]; } -Future parseOptions( +Future parseOptions( PackageMetaProvider packageMetaProvider, List arguments, { - OptionGenerator additionalOptions, + OptionGenerator? additionalOptions, }) async { - var optionSet = await DartdocOptionSet.fromOptionGenerators( + var optionRoot = await DartdocOptionRoot.fromOptionGenerators( 'dartdoc', [ createDartdocOptions, @@ -109,22 +99,22 @@ Future parseOptions( packageMetaProvider); try { - optionSet.parseArguments(arguments); + optionRoot.parseArguments(arguments); } on FormatException catch (e) { stderr.writeln(' fatal error: ${e.message}'); stderr.writeln(''); - _printUsage(optionSet.argParser); + _printUsage(optionRoot.argParser); // Do not use exit() as this bypasses --pause-isolates-on-exit exitCode = 64; return null; } - if (optionSet['help'].valueAtCurrent()) { - _printHelp(optionSet.argParser); + if (optionRoot['help'].valueAtCurrent()) { + _printHelp(optionRoot.argParser); exitCode = 0; return null; } - if (optionSet['version'].valueAtCurrent()) { - _printVersion(optionSet.argParser); + if (optionRoot['version'].valueAtCurrent()) { + _printVersion(optionRoot.argParser); exitCode = 0; return null; } @@ -132,12 +122,12 @@ Future parseOptions( DartdocProgramOptionContext config; try { config = DartdocProgramOptionContext.fromDefaultContextLocation( - optionSet, packageMetaProvider.resourceProvider); + optionRoot, packageMetaProvider.resourceProvider); } on DartdocOptionError catch (e) { stderr.writeln(' fatal error: ${e.message}'); stderr.writeln(''); await stderr.flush(); - _printUsage(optionSet.argParser); + _printUsage(optionRoot.argParser); exitCode = 64; return null; } diff --git a/lib/src/comment_references/model_comment_reference.dart b/lib/src/comment_references/model_comment_reference.dart index 8696006e1c..dd37b972be 100644 --- a/lib/src/comment_references/model_comment_reference.dart +++ b/lib/src/comment_references/model_comment_reference.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // + import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/file_system/file_system.dart'; @@ -16,7 +17,7 @@ abstract class ModelCommentReference { bool get hasConstructorHint; bool get hasCallableHint; List get referenceBy; - Element get staticElement; + Element? get staticElement; /// Construct a [ModelCommentReference] using the analyzer AST. factory ModelCommentReference( @@ -32,45 +33,47 @@ abstract class ModelCommentReference { /// information needed for Dartdoc. Drops link to the [CommentReference] /// and [ResourceProvider] after construction. class _ModelCommentReferenceImpl implements ModelCommentReference { - bool _allowUnnamedConstructor; - void _initAllowCache() { var referencePieces = parsed.whereType().toList(); _allowUnnamedConstructor = false; _allowUnnamedConstructorParameter = false; if (referencePieces.length >= 2) { - IdentifierNode nodeLast; - for (var f in referencePieces) { - if (f.text == nodeLast?.text) { - if (identical(referencePieces.last, f)) { + for (var i = 0; i <= referencePieces.length - 2; i++) { + if (referencePieces[i].text == referencePieces[i + 1].text) { + if (i + 2 == referencePieces.length) { + // This looks like an old-style reference to an unnamed + // constructor, e.g. [lib_name.C.C]. _allowUnnamedConstructor = true; } else { + // This could be a reference to a parameter or type parameter of + // an unnamed/new-declared constructor. _allowUnnamedConstructorParameter = true; } } - nodeLast = f; } - if (referencePieces.last.text == 'new') { + // e.g. [C.new], which may be the unnamed constructor + if (referencePieces.isNotEmpty && referencePieces.last.text == 'new') { _allowUnnamedConstructor = true; } } } + bool? _allowUnnamedConstructor; @override bool get allowUnnamedConstructor { if (_allowUnnamedConstructor == null) { _initAllowCache(); } - return _allowUnnamedConstructor; + return _allowUnnamedConstructor!; } - bool _allowUnnamedConstructorParameter; + bool? _allowUnnamedConstructorParameter; @override bool get allowUnnamedConstructorParameter { if (_allowUnnamedConstructorParameter == null) { _initAllowCache(); } - return _allowUnnamedConstructorParameter; + return _allowUnnamedConstructorParameter!; } @override @@ -93,7 +96,7 @@ class _ModelCommentReferenceImpl implements ModelCommentReference { .toList(growable: false); @override - final Element staticElement; + final Element? staticElement; _ModelCommentReferenceImpl( CommentReference ref, ResourceProvider resourceProvider) @@ -117,7 +120,6 @@ class _ModelCommentReferenceImpl implements ModelCommentReference { .substring(ref.offset - token.offset, ref.end - token.offset); } - List _parsed; - List get parsed => - _parsed ??= CommentReferenceParser(codeRef).parse(); + late final List parsed = + CommentReferenceParser(codeRef).parse(); } diff --git a/lib/src/comment_references/parser.dart b/lib/src/comment_references/parser.dart index c3e5766728..5ed2aea08f 100644 --- a/lib/src/comment_references/parser.dart +++ b/lib/src/comment_references/parser.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // + import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; @@ -47,9 +48,10 @@ class StringTrie { return valid ? index : lastValid; } var matchChar = toMatch.codeUnitAt(index); - if (children.containsKey(matchChar)) { + var matchedChild = children[matchChar]; + if (matchedChild != null) { lastValid = valid ? index : lastValid; - return children[matchChar].match(toMatch, index + 1, lastValid); + return matchedChild.match(toMatch, index + 1, lastValid); } return valid ? index : lastValid; } @@ -58,22 +60,19 @@ class StringTrie { var currentTrie = this; for (var i in toAdd.codeUnits) { currentTrie.children.putIfAbsent(i, () => StringTrie()); - currentTrie = currentTrie.children[i]; + currentTrie = currentTrie.children[i]!; } currentTrie.valid = true; } } -StringTrie _operatorParseTrie; -StringTrie get operatorParseTrie { - if (_operatorParseTrie == null) { - _operatorParseTrie = StringTrie(); - for (var name in operatorNames.keys) { - _operatorParseTrie.addWord(name); - } +final StringTrie operatorParseTrie = () { + var _operatorParseTrie = StringTrie(); + for (var name in operatorNames.keys) { + _operatorParseTrie.addWord(name); } return _operatorParseTrie; -} +}(); /// A parser for comment references. // TODO(jcollins-g): align with [CommentReference] from analyzer AST. @@ -111,7 +110,7 @@ class CommentReferenceParser { return []; } if (prefixResult.type == _PrefixResultType.parsedConstructorHint) { - children.add(prefixResult.node); + children.add(prefixResult.node!); } // [_PrefixResultType.junk] and [_PrefixResultType.missing] we can skip. @@ -127,13 +126,13 @@ class CommentReferenceParser { break; } else if (identifierResult.type == _IdentifierResultType.parsedIdentifier) { - children.add(identifierResult.node); + children.add(identifierResult.node!); var typeVariablesResult = _parseTypeVariables(); if (typeVariablesResult.type == _TypeVariablesResultType.endOfFile) { break; } else if (typeVariablesResult.type == _TypeVariablesResultType.parsedTypeVariables) { - children.add(typeVariablesResult.node); + children.add(typeVariablesResult.node!); } else { assert(typeVariablesResult.type == _TypeVariablesResultType.notTypeVariables); @@ -151,7 +150,7 @@ class CommentReferenceParser { // Invalid trailing junk; reject the reference. return []; } else if (suffixResult.type == _SuffixResultType.parsedCallableHint) { - children.add(suffixResult.node); + children.add(suffixResult.node!); } // [_SuffixResultType.junk] or [_SuffixResultType.missing] we can skip. @@ -206,7 +205,7 @@ class CommentReferenceParser { /// Advances the index forward to the end of the operator if one is /// present and returns the operator's name. Otherwise, leaves _index /// unchanged and returns null. - String _tryParseOperator() { + String? _tryParseOperator() { var tryIndex = _index; if (tryIndex + _operatorKeyword.length < codeRef.length && codeRef.substring(tryIndex, tryIndex + _operatorKeyword.length) == @@ -315,7 +314,6 @@ class CommentReferenceParser { bool _tryMatchLiteral(String characters, {bool acceptTrailingWhitespace = true, bool requireTrailingNonidentifier = false}) { - assert(acceptTrailingWhitespace != null); if (characters.length + _index > _referenceLength) return false; int startIndex; for (startIndex = _index; @@ -385,7 +383,7 @@ enum _PrefixResultType { class _PrefixParseResult { final _PrefixResultType type; - final CommentReferenceNode node; + final CommentReferenceNode? node; const _PrefixParseResult._(this.type, this.node); @@ -413,7 +411,7 @@ enum _IdentifierResultType { class _IdentifierParseResult { final _IdentifierResultType type; - final IdentifierNode node; + final IdentifierNode? node; const _IdentifierParseResult._(this.type, this.node); @@ -437,7 +435,7 @@ enum _TypeVariablesResultType { class _TypeVariablesParseResult { final _TypeVariablesResultType type; - final TypeVariablesNode node; + final TypeVariablesNode? node; const _TypeVariablesParseResult._(this.type, this.node); @@ -464,7 +462,7 @@ enum _SuffixResultType { class _SuffixParseResult { final _SuffixResultType type; - final CommentReferenceNode node; + final CommentReferenceNode? node; const _SuffixParseResult._(this.type, this.node); diff --git a/lib/src/dartdoc.dart b/lib/src/dartdoc.dart new file mode 100644 index 0000000000..9c38593eae --- /dev/null +++ b/lib/src/dartdoc.dart @@ -0,0 +1,515 @@ +// Copyright (c) 2014, 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. + +library src.dartdoc; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' show Platform, exitCode, stderr; + +import 'package:analyzer/file_system/file_system.dart'; +import 'package:dartdoc/options.dart'; +import 'package:dartdoc/src/dartdoc_options.dart'; +import 'package:dartdoc/src/failure.dart'; +import 'package:dartdoc/src/generator/empty_generator.dart'; +import 'package:dartdoc/src/generator/generator.dart'; +import 'package:dartdoc/src/generator/html_generator.dart'; +import 'package:dartdoc/src/generator/markdown_generator.dart'; +import 'package:dartdoc/src/logging.dart'; +import 'package:dartdoc/src/matching_link_result.dart'; +import 'package:dartdoc/src/model/model.dart'; +import 'package:dartdoc/src/package_meta.dart'; +import 'package:dartdoc/src/tuple.dart'; +import 'package:dartdoc/src/utils.dart'; +import 'package:dartdoc/src/version.dart'; +import 'package:dartdoc/src/warnings.dart'; +import 'package:html/parser.dart' show parse; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; + +const String programName = 'dartdoc'; +// Update when pubspec version changes by running `pub run build_runner build` +const String dartdocVersion = packageVersion; + +class DartdocFileWriter implements FileWriter { + final String _outputDir; + @override + final ResourceProvider resourceProvider; + final Map _fileElementMap = {}; + @override + final Set writtenFiles = {}; + + DartdocFileWriter(this._outputDir, this.resourceProvider); + + @override + void writeBytes( + String filePath, + List content, { + bool allowOverwrite = false, + }) { + // Replace '/' separators with proper separators for the platform. + var outFile = path.joinAll(filePath.split('/')); + + if (!allowOverwrite) { + _warnAboutOverwrite(outFile, null); + } + _fileElementMap[outFile] = null; + + var file = _getFile(outFile); + file.writeAsBytesSync(content); + writtenFiles.add(outFile); + logProgress(outFile); + } + + @override + void write(String filePath, String content, {Warnable? element}) { + // Replace '/' separators with proper separators for the platform. + var outFile = path.joinAll(filePath.split('/')); + + _warnAboutOverwrite(outFile, element); + _fileElementMap[outFile] = element; + + var file = _getFile(outFile); + file.writeAsStringSync(content); + writtenFiles.add(outFile); + logProgress(outFile); + } + + void _warnAboutOverwrite(String outFile, Warnable? element) { + if (_fileElementMap.containsKey(outFile)) { + assert(element != null, + 'Attempted overwrite of $outFile without corresponding element'); + var originalElement = _fileElementMap[outFile]; + var referredFrom = []; + if (originalElement != null) referredFrom.add(originalElement); + element?.warn(PackageWarning.duplicateFile, + message: outFile, referredFrom: referredFrom); + } + } + + /// Returns the file at [outFile] relative to [_outputDir], creating the + /// parent directory if necessary. + File _getFile(String outFile) { + var file = resourceProvider + .getFile(resourceProvider.pathContext.join(_outputDir, outFile)); + var parent = file.parent2; + if (!parent.exists) { + parent.create(); + } + return file; + } +} + +/// Generates Dart documentation for all public Dart libraries in the given +/// directory. +class Dartdoc { + Generator _generator; + final PackageBuilder packageBuilder; + final DartdocOptionContext config; + final Set _writtenFiles = {}; + late final Folder _outputDir = config.resourceProvider + .getFolder(config.resourceProvider.pathContext.absolute(config.output)) + ..create(); + + // Fires when the self checks make progress. + final StreamController _onCheckProgress = + StreamController(sync: true); + + Dartdoc._(this.config, this._generator, this.packageBuilder); + + // TODO(srawlins): Remove when https://github.com/dart-lang/linter/issues/2706 + // is fixed. + // ignore: unnecessary_getters_setters + Generator get generator => _generator; + + @visibleForTesting + // TODO(srawlins): Remove when https://github.com/dart-lang/linter/issues/2706 + // is fixed. + // ignore: unnecessary_getters_setters + set generator(Generator newGenerator) => _generator = newGenerator; + + /// Asynchronous factory method that builds Dartdoc with an empty generator. + static Future withEmptyGenerator( + DartdocOptionContext config, + PackageBuilder packageBuilder, + ) async { + return Dartdoc._( + config, + await initEmptyGenerator(config), + packageBuilder, + ); + } + + /// Asynchronous factory method that builds Dartdoc with a generator + /// determined by the given context. + static Future fromContext( + DartdocGeneratorOptionContext context, + PackageBuilder packageBuilder, + ) async { + Generator generator; + switch (context.format) { + case 'html': + generator = await initHtmlGenerator(context); + break; + case 'md': + generator = await initMarkdownGenerator(context); + break; + default: + throw DartdocFailure('Unsupported output format: ${context.format}'); + } + return Dartdoc._( + context, + generator, + packageBuilder, + ); + } + + Stream get onCheckProgress => _onCheckProgress.stream; + + @visibleForTesting + Future generateDocsBase() async { + var stopwatch = Stopwatch()..start(); + var packageGraph = await packageBuilder.buildPackageGraph(); + var seconds = stopwatch.elapsedMilliseconds / 1000.0; + var libs = packageGraph.libraries.length; + logInfo("Initialized dartdoc with $libs librar${libs == 1 ? 'y' : 'ies'} " + 'in ${seconds.toStringAsFixed(1)} seconds'); + stopwatch.reset(); + + // Create the out directory. + if (!_outputDir.exists) _outputDir.create(); + + var writer = DartdocFileWriter(_outputDir.path, config.resourceProvider); + await generator.generate(packageGraph, writer); + + _writtenFiles.addAll(writer.writtenFiles); + if (config.validateLinks && _writtenFiles.isNotEmpty) { + _validateLinks(packageGraph, _outputDir.path); + } + + var warnings = packageGraph.packageWarningCounter.warningCount; + var errors = packageGraph.packageWarningCounter.errorCount; + if (warnings == 0 && errors == 0) { + logInfo('no issues found'); + } else { + logWarning("Found $warnings ${pluralize('warning', warnings)} " + "and $errors ${pluralize('error', errors)}."); + } + + seconds = stopwatch.elapsedMilliseconds / 1000.0; + libs = packageGraph.localPublicLibraries.length; + logInfo("Documented $libs public librar${libs == 1 ? 'y' : 'ies'} " + 'in ${seconds.toStringAsFixed(1)} seconds'); + + if (config.showStats) { + logInfo(markdownStats.buildReport()); + } + return DartdocResults(config.topLevelPackageMeta, packageGraph, _outputDir); + } + + /// Generate Dartdoc documentation. + /// + /// [DartdocResults] is returned if dartdoc succeeds. [DartdocFailure] is + /// thrown if dartdoc fails in an expected way, for example if there is an + /// analysis error in the code. + Future generateDocs() async { + DartdocResults? dartdocResults; + try { + logInfo('Documenting ${config.topLevelPackageMeta}...'); + + dartdocResults = await generateDocsBase(); + if (dartdocResults.packageGraph.localPublicLibraries.isEmpty) { + logWarning('dartdoc could not find any libraries to document'); + } + + final errorCount = + dartdocResults.packageGraph.packageWarningCounter.errorCount; + if (errorCount > 0) { + throw DartdocFailure('encountered $errorCount errors'); + } + var outDirPath = config.resourceProvider.pathContext + .absolute(dartdocResults.outDir.path); + logInfo('Success! Docs generated into $outDirPath'); + return dartdocResults; + } finally { + dartdocResults?.packageGraph.dispose(); + } + } + + /// Warn on file paths. + void _warn(PackageGraph packageGraph, PackageWarning kind, String warnOn, + String origin, + {String? referredFrom}) { + // Ordinarily this would go in [Package.warn], but we don't actually know what + // ModelElement to warn on yet. + Warnable? warnOnElement; + var referredFromElements = {}; + Set? warnOnElements; + + // Make all paths relative to origin. + if (path.isWithin(origin, warnOn)) { + warnOn = path.relative(warnOn, from: origin); + } + if (referredFrom != null) { + if (path.isWithin(origin, referredFrom)) { + referredFrom = path.relative(referredFrom, from: origin); + } + var hrefReferredFrom = _hrefs[referredFrom]; + // Source paths are always relative. + if (hrefReferredFrom != null) { + referredFromElements.addAll(hrefReferredFrom); + } + } + warnOnElements = _hrefs[warnOn]; + + if (referredFromElements.any((e) => e.isCanonical)) { + referredFromElements.removeWhere((e) => !e.isCanonical); + } + if (warnOnElements != null) { + for (var e in warnOnElements) { + if (e.isCanonical) { + warnOnElement = e; + break; + } + } + } + + if (referredFromElements.isEmpty && referredFrom == 'index.html') { + referredFromElements.add(packageGraph.defaultPackage); + } + var message = warnOn; + if (referredFrom == 'index.json') message = '$warnOn (from index.json)'; + packageGraph.warnOnElement(warnOnElement, kind, + message: message, referredFrom: referredFromElements); + } + + void _doOrphanCheck( + PackageGraph packageGraph, String origin, Set visited) { + var normalOrigin = path.normalize(origin); + var staticAssets = path.joinAll([normalOrigin, 'static-assets', '']); + var indexJson = path.joinAll([normalOrigin, 'index.json']); + var foundIndexJson = false; + + void checkDirectory(Folder dir) { + for (var f in dir.getChildren()) { + if (f is Folder) { + checkDirectory(f); + continue; + } + var fullPath = path.normalize(f.path); + if (fullPath.startsWith(staticAssets)) { + continue; + } + if (path.equals(fullPath, indexJson)) { + foundIndexJson = true; + _onCheckProgress.add(fullPath); + continue; + } + if (visited.contains(fullPath)) continue; + var relativeFullPath = path.relative(fullPath, from: normalOrigin); + if (!_writtenFiles.contains(relativeFullPath)) { + // This isn't a file we wrote (this time); don't claim we did. + _warn( + packageGraph, PackageWarning.unknownFile, fullPath, normalOrigin); + } else { + // Error messages are orphaned by design and do not appear in the search + // index. + if (const {'__404error.html', 'categories.json'}.contains(fullPath)) { + _warn(packageGraph, PackageWarning.orphanedFile, fullPath, + normalOrigin); + } + } + _onCheckProgress.add(fullPath); + } + } + + checkDirectory(config.resourceProvider.getFolder(normalOrigin)); + + if (!foundIndexJson) { + _warn(packageGraph, PackageWarning.brokenLink, indexJson, normalOrigin); + _onCheckProgress.add(indexJson); + } + } + + // This is extracted to save memory during the check; be careful not to hang + // on to anything referencing the full file and doc tree. + Tuple2, String?>? _getStringLinksAndHref(String fullPath) { + var file = config.resourceProvider.getFile(fullPath); + if (!file.exists) { + return null; + } + // TODO(srawlins): It is possible that instantiating an HtmlParser using + // `lowercaseElementName: false` and `lowercaseAttrName: false` may save + // time or memory. + var doc = parse(file.readAsBytesSync()); + var base = doc.querySelector('base'); + String? baseHref; + if (base != null) { + baseHref = base.attributes['href']; + } + var links = doc.querySelectorAll('a'); + var stringLinks = links + .map((link) => link.attributes['href']) + .whereType() + .where((href) => href.isNotEmpty) + .toList(); + + return Tuple2(stringLinks, baseHref); + } + + void _doSearchIndexCheck( + PackageGraph packageGraph, String origin, Set visited) { + var fullPath = path.joinAll([origin, 'index.json']); + var indexPath = path.joinAll([origin, 'index.html']); + var file = config.resourceProvider.getFile(fullPath); + if (!file.exists) { + return; + } + var decoder = JsonDecoder(); + List jsonData = decoder.convert(file.readAsStringSync()); + + var found = {}; + found.add(fullPath); + // The package index isn't supposed to be in the search, so suppress the + // warning. + found.add(indexPath); + for (Map entry in jsonData) { + if (entry.containsKey('href')) { + var entryPath = + path.joinAll([origin, ...path.posix.split(entry['href'])]); + if (!visited.contains(entryPath)) { + _warn(packageGraph, PackageWarning.brokenLink, entryPath, + path.normalize(origin), + referredFrom: fullPath); + } + found.add(entryPath); + } + } + // Missing from search index + var missingFromSearch = visited.difference(found); + for (var s in missingFromSearch) { + _warn(packageGraph, PackageWarning.missingFromSearchIndex, s, + path.normalize(origin), + referredFrom: fullPath); + } + } + + void _doCheck(PackageGraph packageGraph, String origin, Set visited, + String pathToCheck, + [String? source, String? fullPath]) { + fullPath ??= path.normalize(path.joinAll([origin, pathToCheck])); + + var stringLinksAndHref = _getStringLinksAndHref(fullPath); + if (stringLinksAndHref == null) { + _warn(packageGraph, PackageWarning.brokenLink, pathToCheck, + path.normalize(origin), + referredFrom: source); + _onCheckProgress.add(pathToCheck); + // Remove so that we properly count that the file doesn't exist for + // the orphan check. + visited.remove(fullPath); + return; + } + visited.add(fullPath); + var stringLinks = stringLinksAndHref.item1; + var baseHref = stringLinksAndHref.item2; + + // Prevent extremely large stacks by storing the paths we are using + // here instead -- occasionally, very large jobs have overflowed + // the stack without this. + // (newPathToCheck, newFullPath) + var toVisit = >{}; + + final ignoreHyperlinks = RegExp(r'^(https:|http:|mailto:|ftp:)'); + for (final href in stringLinks) { + if (!href.startsWith(ignoreHyperlinks)) { + final uri = Uri.tryParse(href); + + if (uri == null || !uri.hasAuthority && !uri.hasFragment) { + String full; + if (baseHref != null) { + full = '${path.dirname(pathToCheck)}/$baseHref/$href'; + } else { + full = '${path.dirname(pathToCheck)}/$href'; + } + + final newPathToCheck = path.normalize(full); + final newFullPath = + path.normalize(path.joinAll([origin, newPathToCheck])); + if (!visited.contains(newFullPath)) { + toVisit.add(Tuple2(newPathToCheck, newFullPath)); + visited.add(newFullPath); + } + } + } + } + for (var visitPaths in toVisit) { + _doCheck(packageGraph, origin, visited, visitPaths.item1, pathToCheck, + visitPaths.item2); + } + _onCheckProgress.add(pathToCheck); + } + + late final Map> _hrefs; + + /// Don't call this method more than once, and only after you've + /// generated all docs for the Package. + void _validateLinks(PackageGraph packageGraph, String origin) { + _hrefs = packageGraph.allHrefs; + + final visited = {}; + logInfo('Validating docs...'); + _doCheck(packageGraph, origin, visited, 'index.html'); + _doOrphanCheck(packageGraph, origin, visited); + _doSearchIndexCheck(packageGraph, origin, visited); + } + + /// Runs [generateDocs] function and properly handles the errors. + /// + /// Passing in a [postProcessCallback] to do additional processing after + /// the documentation is generated. + void executeGuarded([ + Future Function(DartdocOptionContext)? postProcessCallback, + ]) { + onCheckProgress.listen(logProgress); + // This function should *never* await `runZonedGuarded` because the errors + // thrown in generateDocs are uncaught. We want this because uncaught errors + // cause IDE debugger to automatically stop at the exception. + // + // If you await the zone, the code that comes after the await is not + // executed if the zone dies due to uncaught error. To avoid this confusion, + // never await `runZonedGuarded` and never change the return value of + // [executeGuarded]. + runZonedGuarded( + () async { + await generateDocs(); + await postProcessCallback?.call(config); + }, + (e, chain) { + if (e is DartdocFailure) { + stderr.writeln('\n$_dartdocFailedMessage: $e.'); + exitCode = 1; + } else { + stderr.writeln('\n$_dartdocFailedMessage: $e\n$chain'); + exitCode = 255; + } + }, + zoneSpecification: ZoneSpecification( + print: (_, __, ___, String line) => logPrint(line), + ), + ); + } +} + +/// The results of a [Dartdoc.generateDocs] call. +class DartdocResults { + final PackageMeta packageMeta; + final PackageGraph packageGraph; + final Folder outDir; + + DartdocResults(this.packageMeta, this.packageGraph, this.outDir); +} + +String get _dartdocFailedMessage => + 'dartdoc $packageVersion (${Platform.script.path}) failed'; diff --git a/lib/src/dartdoc_options.dart b/lib/src/dartdoc_options.dart index 778b667b1a..a1ce493077 100644 --- a/lib/src/dartdoc_options.dart +++ b/lib/src/dartdoc_options.dart @@ -42,7 +42,7 @@ const bool _kBoolVal = true; const String compileArgsTagName = 'compile_args'; -int get _usageLineLength => stdout.hasTerminal ? stdout.terminalColumns : null; +int get _usageLineLength => stdout.hasTerminal ? stdout.terminalColumns : 80; typedef ConvertYamlToType = T Function(YamlMap, String, ResourceProvider); @@ -58,20 +58,20 @@ class DartdocFileMissing extends DartdocOptionError { /// the 'categories' keyword in the options file, and populated by the /// [CategoryConfiguration] class. class CategoryDefinition { - /// Internal name of the category. - final String name; + /// Internal name of the category, or null for the default category. + final String? name; /// Displayed name of the category in docs, or null if there is none. - final String _displayName; + final String? _displayName; /// Canonical path of the markdown file used to document this category /// (or null if undocumented). - final String documentationMarkdown; + final String? documentationMarkdown; CategoryDefinition(this.name, this._displayName, this.documentationMarkdown); /// Returns the [_displayName], if available, or else simply [name]. - String get displayName => _displayName ?? name; + String get displayName => _displayName ?? name ?? ''; } /// A configuration class that can interpret category definitions from a YAML @@ -91,12 +91,10 @@ class CategoryConfiguration { var newCategoryDefinitions = {}; for (var entry in yamlMap.entries) { var name = entry.key.toString(); - String displayName; - String documentationMarkdown; var categoryMap = entry.value; if (categoryMap is Map) { - displayName = categoryMap['displayName']?.toString(); - documentationMarkdown = categoryMap['markdown']?.toString(); + var displayName = categoryMap['displayName']?.toString(); + var documentationMarkdown = categoryMap['markdown']?.toString(); if (documentationMarkdown != null) { documentationMarkdown = resourceProvider.pathContext.canonicalize( resourceProvider.pathContext @@ -121,9 +119,7 @@ class ToolConfiguration { final ResourceProvider resourceProvider; - ToolRunner _runner; - - ToolRunner get runner => _runner ??= ToolRunner(this); + late ToolRunner runner = ToolRunner(this); ToolConfiguration._(this.tools, this.resourceProvider); @@ -139,14 +135,14 @@ class ToolConfiguration { for (var entry in yamlMap.entries) { var name = entry.key.toString(); var toolMap = entry.value; - List compileArgs; - String description; - List command; - List setupCommand; + String? description; + List? compileArgs; + List? command; + List? setupCommand; if (toolMap is Map) { - description = toolMap['description']?.toString(); - List findCommand([String prefix = '']) { - List command; + description = toolMap['description'].toString(); + List? findCommand([String prefix = '']) { + List? command; // If the command key is given, then it applies to all platforms. var commandFromKey = toolMap.containsKey('${prefix}command') ? '${prefix}command' @@ -182,8 +178,8 @@ class ToolConfiguration { command = findCommand(); setupCommand = findCommand('setup_'); - List findArgs() { - List args; + List? findArgs() { + List? args; if (toolMap.containsKey(compileArgsTagName)) { var compileArgs = toolMap[compileArgsTagName]; if (compileArgs is String) { @@ -247,7 +243,10 @@ class ToolConfiguration { setupCommand; } newToolDefinitions[name] = ToolDefinition.fromCommand( - [executable] + command, setupCommand, description, resourceProvider, + [executable] + command, + setupCommand ?? const [], + description, + resourceProvider, compileArgs: compileArgs ?? const []); } return ToolConfiguration._(newToolDefinitions, resourceProvider); @@ -257,7 +256,7 @@ class ToolConfiguration { /// A container class to keep track of where our yaml data came from. class _YamlFileData { /// The map from the yaml file. - final Map data; + final Map data; /// The path to the directory containing the yaml file. final String canonicalDirectoryPath; @@ -288,7 +287,7 @@ class _OptionValueWithContext { String canonicalDirectoryPath; /// If non-null, the basename of the configuration file the value came from. - String definingFile; + String? definingFile; /// A [pathLib.Context] variable initialized with canonicalDirectoryPath. p.Context pathContext; @@ -297,10 +296,9 @@ class _OptionValueWithContext { /// /// [path] is the path where this value came from (not required to be /// canonical). - _OptionValueWithContext(this.value, String path, {this.definingFile}) { - canonicalDirectoryPath = p.canonicalize(path); - pathContext = p.Context(current: canonicalDirectoryPath); - } + _OptionValueWithContext(this.value, String path, {this.definingFile}) + : canonicalDirectoryPath = p.canonicalize(path), + pathContext = p.Context(current: p.canonicalize(path)); /// Assume value is a path, and attempt to resolve it. /// @@ -338,9 +336,9 @@ class _OptionValueWithContext { /// /// Use via implementations [DartdocOptionSet], [DartdocOptionArgFile], /// [DartdocOptionArgOnly], and [DartdocOptionFileOnly]. -abstract class DartdocOption { +abstract class DartdocOption { /// This is the value returned if we couldn't find one otherwise. - final T defaultsTo; + final T? defaultsTo; /// Text string for help passed on in command line options. final String help; @@ -378,7 +376,7 @@ abstract class DartdocOption { } /// Closure to convert yaml data into some other structure. - ConvertYamlToType _convertYamlToType; + final ConvertYamlToType? _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 @@ -397,11 +395,6 @@ abstract class DartdocOption { 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 __yamlAtCanonicalPathCache = {}; /// Implementation detail for [DartdocOptionFileOnly]. Make sure we use @@ -409,19 +402,6 @@ abstract class DartdocOption { Map get _yamlAtCanonicalPathCache => root.__yamlAtCanonicalPathCache; - final ArgParser __argParser = ArgParser(usageLineLength: _usageLineLength); - - 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 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( @@ -431,14 +411,14 @@ abstract class DartdocOption { void _validatePaths(_OptionValueWithContext valueWithContext) { if (!mustExist) return; assert(isDir || isFile); - List resolvedPaths; + var resolvedPaths = []; var value = valueWithContext.value; if (value is String) { resolvedPaths = [valueWithContext.resolvedValue as String]; } else if (value is List) { resolvedPaths = valueWithContext.resolvedValue as List; } else if (value is Map) { - resolvedPaths = (valueWithContext.resolvedValue as Map).values.toList(); + resolvedPaths = [...(valueWithContext.resolvedValue as Map).values]; } else { assert( false, @@ -457,11 +437,11 @@ abstract class DartdocOption { /// For a [List] or [String] value, if [isDir] or [isFile] is set, /// resolve paths in value relative to canonicalPath. - T _handlePathsInContext(_OptionValueWithContext valueWithContext) { + T? _handlePathsInContext(_OptionValueWithContext? valueWithContext) { if (valueWithContext?.value == null || !(isDir || isFile || isGlob)) { return valueWithContext?.value; } - _validatePaths(valueWithContext); + _validatePaths(valueWithContext!); return valueWithContext.resolvedValue; } @@ -472,17 +452,14 @@ abstract class DartdocOption { 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; - } + /// To avoid accessing early, call [add] on the option's parent before + /// looking up unless this is a [DartdocRootOption]. + late final DartdocOption parent; - /// The root [DartdocOption] containing this object, or [this] if the object - /// has no parent. - DartdocOption get root { - DartdocOption p = this; - while (p.parent != null) { + /// The [DartdocOptionRoot] containing this object. + DartdocOptionRoot get root { + DartdocOption p = this; + while (p is! DartdocOptionRoot) { p = p.parent; } return p; @@ -491,11 +468,12 @@ abstract class DartdocOption { /// All object names starting at the root. Iterable get keys { var keyList = []; - DartdocOption option = this; - while (option?.name != null) { + DartdocOption option = this; + while (option is! DartdocOptionRoot) { keyList.add(option.name); option = option.parent; } + keyList.add(option.name); return keyList.reversed; } @@ -513,23 +491,17 @@ abstract class DartdocOption { /// 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(Folder dir); + // TODO(jcollins-g): use of dynamic. https://github.com/dart-lang/dartdoc/issues/2814 + dynamic valueAt(Folder dir); /// Calls [valueAt] with the working directory at the start of the program. - T valueAtCurrent() => valueAt(_directoryCurrent); - - Folder __directoryCurrent; - Folder get _directoryCurrent => - __directoryCurrent ??= resourceProvider.getFolder(_directoryCurrentPath); - - String __directoryCurrentPath; - String get _directoryCurrentPath => - __directoryCurrentPath ??= resourceProvider.pathContext.current; + // TODO(jcollins-g): use of dynamic. https://github.com/dart-lang/dartdoc/issues/2814 + dynamic valueAtCurrent() => valueAt(_directoryCurrent); - /// Calls [valueAt] on the directory this element is defined in. - T valueAtElement(Element element) => - valueAt(resourceProvider.getFolder(resourceProvider.pathContext.normalize( - resourceProvider.pathContext.basename(element.source.fullName)))); + late final Folder _directoryCurrent = + resourceProvider.getFolder(_directoryCurrentPath); + late final String _directoryCurrentPath = + resourceProvider.pathContext.current; /// Adds a DartdocOption to the children of this DartdocOption. void add(DartdocOption option) { @@ -538,21 +510,26 @@ abstract class DartdocOption { 'Tried to add two children with the same name: ${option.name}'); } _children[option.name] = option; + // TODO(jcollins-g): Consider a stronger refactor that doesn't rely on + // post-construction setup for [parent]. option.parent = this; - option.traverse((option) => option._onAdd()); } - /// This method is guaranteed to be called when [this] or any parent is added. - void _onAdd() {} + /// This method is called when parsing options to set up the [ArgParser]. + void _addToArgParser(ArgParser argParser) {} /// Adds a list of dartdoc options to the children of this DartdocOption. void addAll(Iterable options) => options.forEach(add); /// Get the immediate child of this node named [name]. - DartdocOption operator [](String name) { - return _children[name]; + DartdocOption operator [](String name) { + return _children[name]!; } + /// Get the immediate child of this node named [name] and its value at [dir]. + U getValueAs(String name, Folder dir) => + _children[name]?.valueAt(dir) as U; + /// Apply the function [visit] to [this] and all children. void traverse(void Function(DartdocOption option) visit) { visit(this); @@ -566,7 +543,8 @@ abstract class DartdocOption { /// overridden by a file. class DartdocOptionFileSynth extends DartdocOption with DartdocSyntheticOption, _DartdocFileOption { - bool _parentDirOverridesChild; + @override + final bool parentDirOverridesChild; @override final T Function(DartdocSyntheticOption, Folder) _compute; @@ -575,15 +553,13 @@ class DartdocOptionFileSynth extends DartdocOption {bool mustExist = false, String help = '', OptionKind optionIs = OptionKind.other, - bool parentDirOverridesChild, - ConvertYamlToType convertYamlToType}) + this.parentDirOverridesChild = false, + ConvertYamlToType? convertYamlToType}) : super(name, null, help, optionIs, mustExist, convertYamlToType, - resourceProvider) { - _parentDirOverridesChild = parentDirOverridesChild; - } + resourceProvider); @override - T valueAt(Folder dir) { + T? valueAt(Folder dir) { var result = _valueAtFromFile(dir); if (result?.definingFile != null) { return _handlePathsInContext(result); @@ -600,37 +576,35 @@ class DartdocOptionFileSynth extends DartdocOption _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 + final String? abbr; + @override + final bool hide; + @override + final bool negatable; + @override + final bool splitCommas; @override final T Function(DartdocSyntheticOption, Folder) _compute; DartdocOptionArgSynth( String name, this._compute, ResourceProvider resourceProvider, - {String abbr, + {this.abbr, bool mustExist = false, String help = '', - bool hide = false, + this.hide = false, OptionKind optionIs = OptionKind.other, - bool negatable = false, - bool splitCommas}) + this.negatable = false, + this.splitCommas = false}) : super(name, null, help, optionIs, mustExist, null, resourceProvider) { - _hide = hide; - _negatable = negatable; - _splitCommas = splitCommas; - _abbr = abbr; + assert(T is! Iterable || splitCommas == true); } @override @@ -640,24 +614,12 @@ class DartdocOptionArgSynth extends DartdocOption } @override - T valueAt(Folder dir) { + T? valueAt(Folder 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 @@ -682,9 +644,9 @@ abstract class DartdocSyntheticOption implements DartdocOption { T Function(DartdocSyntheticOption, Folder) get _compute; @override - T valueAt(Folder dir) => _valueAtFromSynthetic(dir); + T? valueAt(Folder dir) => _valueAtFromSynthetic(dir); - T _valueAtFromSynthetic(Folder dir) { + T? _valueAtFromSynthetic(Folder dir) { var context = _OptionValueWithContext(_compute(this, dir), dir.path); return _handlePathsInContext(context); } @@ -706,12 +668,13 @@ abstract class DartdocSyntheticOption implements DartdocOption { typedef OptionGenerator = Future> Function( PackageMetaProvider); -/// A [DartdocOption] that only contains other [DartdocOption]s and is not an -/// option itself. -class DartdocOptionSet extends DartdocOption { - DartdocOptionSet(String name, ResourceProvider resourceProvider) - : super( - name, null, null, OptionKind.other, false, null, resourceProvider); +/// This is a [DartdocOptionSet] used as a root node. +class DartdocOptionRoot extends DartdocOptionSet { + DartdocOptionRoot(String name, ResourceProvider resourceProvider) + : super(name, resourceProvider); + + late final ArgParser _argParser = + ArgParser(usageLineLength: _usageLineLength); /// Asynchronous factory that is the main entry point to initialize Dartdoc /// options for use. @@ -719,26 +682,37 @@ class DartdocOptionSet extends DartdocOption { /// [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 fromOptionGenerators( + static Future fromOptionGenerators( String name, Iterable optionGenerators, PackageMetaProvider packageMetaProvider) async { var optionSet = - DartdocOptionSet(name, packageMetaProvider.resourceProvider); + DartdocOptionRoot(name, packageMetaProvider.resourceProvider); for (var generator in optionGenerators) { optionSet.addAll(await generator(packageMetaProvider)); } return optionSet; } - /// [DartdocOptionSet] always has the null value. - @override - void valueAt(Folder dir) {} + ArgParser get argParser => _argParser; - /// Since we have no value, [_onMissing] does nothing. - @override - void _onMissing( - _OptionValueWithContext valueWithContext, String missingFilename) {} + /// Initialized via [_parseArguments]. + late ArgResults __argResults; + + bool _argParserInitialized = false; + + /// 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 arguments) { + if (!_argParserInitialized) { + _argParserInitialized = true; + traverse((DartdocOption option) { + option._addToArgParser(argParser); + }); + } + __argResults = argParser.parse(arguments); + } /// Traverse skips this node, because it doesn't represent a real /// configuration object. @@ -748,75 +722,81 @@ class DartdocOptionSet extends DartdocOption { value.traverse(visitor); } } + + @override + DartdocOption get parent => + throw UnsupportedError('Root nodes have no parent'); +} + +/// A [DartdocOption] that only contains other [DartdocOption]s and is not an +/// option itself. +class DartdocOptionSet extends DartdocOption { + DartdocOptionSet(String name, ResourceProvider resourceProvider) + : super(name, null, '', OptionKind.other, false, null, resourceProvider); + + /// [DartdocOptionSet] always has the null value. + @override + void valueAt(Folder dir) {} + + /// Since we have no value, [_onMissing] does nothing. + @override + void _onMissing( + _OptionValueWithContext valueWithContext, String missingFilename) {} } /// A [DartdocOption] that only exists as a command line argument. `--help` is a /// good example. class DartdocOptionArgOnly extends DartdocOption with _DartdocArgOption { - String _abbr; - bool _hide; - bool _negatable; - bool _splitCommas; + @override + final String? abbr; + @override + final bool hide; + @override + final bool negatable; + @override + final bool splitCommas; DartdocOptionArgOnly( String name, T defaultsTo, ResourceProvider resourceProvider, - {String abbr, + {this.abbr, bool mustExist = false, String help = '', - bool hide = false, + this.hide = false, OptionKind optionIs = OptionKind.other, - bool negatable = false, - bool splitCommas}) + this.negatable = false, + this.splitCommas = false}) : super(name, defaultsTo, help, optionIs, mustExist, null, - resourceProvider) { - _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; + resourceProvider); } /// A [DartdocOption] that works with command line arguments and /// `dartdoc_options` files. class DartdocOptionArgFile extends DartdocOption with _DartdocArgOption, _DartdocFileOption { - String _abbr; - bool _hide; - bool _negatable; - bool _parentDirOverridesChild; - bool _splitCommas; + @override + final String? abbr; + @override + final bool hide; + @override + final bool negatable; + @override + final bool parentDirOverridesChild; + @override + final bool splitCommas; DartdocOptionArgFile( String name, T defaultsTo, ResourceProvider resourceProvider, - {String abbr, + {this.abbr, bool mustExist = false, String help = '', - bool hide = false, + this.hide = false, OptionKind optionIs = OptionKind.other, - bool negatable = false, - bool parentDirOverridesChild = false, - bool splitCommas}) + this.negatable = false, + this.parentDirOverridesChild = false, + this.splitCommas = false}) : super(name, defaultsTo, help, optionIs, mustExist, null, - resourceProvider) { - _abbr = abbr; - _hide = hide; - _negatable = negatable; - _parentDirOverridesChild = parentDirOverridesChild; - _splitCommas = splitCommas; - } + resourceProvider); @override void _onMissing( @@ -831,47 +811,28 @@ class DartdocOptionArgFile extends DartdocOption /// Try to find an explicit argument setting this value, but if not, fall back /// to files finally, the default. @override - T valueAt(Folder dir) { + T? valueAt(Folder dir) { var value = _valueAtFromArgs(); value ??= _valueAtFromFiles(dir); 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 extends DartdocOption with _DartdocFileOption { - bool _parentDirOverridesChild; + @override + final bool parentDirOverridesChild; DartdocOptionFileOnly( String name, T defaultsTo, ResourceProvider resourceProvider, {bool mustExist = false, String help = '', OptionKind optionIs = OptionKind.other, - bool parentDirOverridesChild = false, - ConvertYamlToType convertYamlToType}) + this.parentDirOverridesChild = false, + ConvertYamlToType? convertYamlToType}) : super(name, defaultsTo, help, optionIs, mustExist, convertYamlToType, - resourceProvider) { - _parentDirOverridesChild = parentDirOverridesChild; - } - - @override - bool get parentDirOverridesChild => _parentDirOverridesChild; + resourceProvider); } /// Implements checking for options contained in dartdoc.yaml. @@ -908,18 +869,18 @@ abstract class _DartdocFileOption implements DartdocOption { /// Searches for a value in configuration files relative to [dir], and if not /// found, returns [defaultsTo]. - T valueAt(Folder dir) { + T? valueAt(Folder dir) { return _valueAtFromFiles(dir) ?? defaultsTo; } - final Map __valueAtFromFiles = {}; + final Map __valueAtFromFiles = {}; // The value of this option from files will not change unless files are // modified during execution (not allowed in Dartdoc). - T _valueAtFromFiles(Folder dir) { + T? _valueAtFromFiles(Folder dir) { var key = resourceProvider.pathContext.canonicalize(dir.path); if (!__valueAtFromFiles.containsKey(key)) { - _OptionValueWithContext valueWithContext; + _OptionValueWithContext? valueWithContext; if (parentDirOverridesChild) { valueWithContext = _valueAtFromFilesLastFound(dir); } else { @@ -932,8 +893,8 @@ abstract class _DartdocFileOption implements DartdocOption { /// Searches all dartdoc_options files through parent directories, starting at /// [dir], for the option and returns one once found. - _OptionValueWithContext _valueAtFromFilesFirstFound(Folder folder) { - _OptionValueWithContext value; + _OptionValueWithContext? _valueAtFromFilesFirstFound(Folder folder) { + _OptionValueWithContext? value; for (var dir in folder.withAncestors) { value = _valueAtFromFile(dir); if (value != null) break; @@ -944,8 +905,8 @@ abstract class _DartdocFileOption implements DartdocOption { /// 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(Folder folder) { - _OptionValueWithContext value; + _OptionValueWithContext? _valueAtFromFilesLastFound(Folder folder) { + _OptionValueWithContext? value; for (var dir in folder.withAncestors) { var tmpValue = _valueAtFromFile(dir); if (tmpValue != null) value = tmpValue; @@ -955,16 +916,16 @@ abstract class _DartdocFileOption implements DartdocOption { /// Returns null if not set in the YAML file in this directory (or its /// parents). - _OptionValueWithContext _valueAtFromFile(Folder dir) { + _OptionValueWithContext? _valueAtFromFile(Folder dir) { var yamlFileData = _yamlAtDirectory(dir); var contextPath = yamlFileData.canonicalDirectoryPath; - Object yamlData = yamlFileData.data ?? {}; + Object yamlData = yamlFileData.data; for (var key in keys) { if (yamlData is Map && !yamlData.containsKey(key)) return null; yamlData = (yamlData as Map)[key] ?? {}; } - Object returnData; + Object? returnData; if (_isListString) { if (yamlData is YamlList) { returnData = [ @@ -972,6 +933,7 @@ abstract class _DartdocFileOption implements DartdocOption { ]; } } else if (yamlData is YamlMap) { + var convertYamlToType = _convertYamlToType; // 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. @@ -979,8 +941,8 @@ abstract class _DartdocFileOption implements DartdocOption { // 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, String canonicalYamlPath, + if (_isMapString && convertYamlToType == null) { + convertYamlToType = (YamlMap yamlMap, String canonicalYamlPath, ResourceProvider resourceProvider) { var returnData = {}; for (var entry in yamlMap.entries) { @@ -989,15 +951,15 @@ abstract class _DartdocFileOption implements DartdocOption { return returnData as T; }; } - if (_convertYamlToType == null) { + if (convertYamlToType == null) { throw DartdocOptionError( 'Unable to convert yaml to type for option: $fieldName, method not ' 'defined'); } var canonicalDirectoryPath = resourceProvider.pathContext.canonicalize(contextPath); - returnData = _convertYamlToType( - yamlData, canonicalDirectoryPath, resourceProvider); + returnData = + convertYamlToType(yamlData, canonicalDirectoryPath, resourceProvider); } else if (_isDouble) { if (yamlData is num) { returnData = yamlData.toDouble(); @@ -1021,7 +983,7 @@ abstract class _DartdocFileOption implements DartdocOption { var canonicalPath = resourceProvider.pathContext.canonicalize(folder.path); if (_yamlAtCanonicalPathCache.containsKey(canonicalPath)) { - yamlData = _yamlAtCanonicalPathCache[canonicalPath]; + yamlData = _yamlAtCanonicalPathCache[canonicalPath]!; break; } canonicalPaths.add(canonicalPath); @@ -1059,12 +1021,12 @@ abstract class _DartdocArgOption implements DartdocOption { /// For [ArgParser], set to a single character to have a short version of the /// command line argument. - String get abbr; + 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(Folder dir) => _valueAtFromArgs() ?? defaultsTo; + T? valueAt(Folder dir) => _valueAtFromArgs() ?? defaultsTo; /// For passing in to [int.parse] and [double.parse] `onError'. void _throwErrorForTypes(String value) { @@ -1075,6 +1037,9 @@ abstract class _DartdocArgOption implements DartdocOption { example = '32'; } else if (_isDouble) { example = '0.76'; + } else { + throw UnimplementedError( + 'Type for $name is not implemented in $_throwErrorForTypes'); } throw DartdocOptionError( 'Invalid argument value: --$argName, set to "$value", must be a ' @@ -1082,7 +1047,7 @@ abstract class _DartdocArgOption implements DartdocOption { } /// Returns null if no argument was given on the command line. - T _valueAtFromArgs() { + T? _valueAtFromArgs() { var valueWithContext = _valueAtFromArgsWithContext(); return _handlePathsInContext(valueWithContext); } @@ -1103,37 +1068,38 @@ abstract class _DartdocArgOption implements DartdocOption { /// the [argParser] and the working directory from [_directoryCurrent]. /// /// Throws [UnsupportedError] if [T] is not a supported type. - _OptionValueWithContext _valueAtFromArgsWithContext() { + _OptionValueWithContext? _valueAtFromArgsWithContext() { if (!_argResults.wasParsed(argName)) return null; - T retval; // Unlike in _DartdocFileOption, we throw here on inputs being invalid // rather than silently proceeding. This is because the user presumably // typed something wrong on the command line and can therefore fix it. // dartdoc_option.yaml files from other packages may not be fully in the // user's control. if (_isBool || _isListString || _isString) { - retval = _argResults[argName]; + return _OptionValueWithContext( + _argResults[argName], _directoryCurrentPath); } else if (_isInt) { - retval = int.tryParse(_argResults[argName]) as T; - if (retval == null) _throwErrorForTypes(_argResults[argName]); + var value = int.tryParse(_argResults[argName]); + if (value == null) _throwErrorForTypes(_argResults[argName]); + return _OptionValueWithContext(value as T, _directoryCurrentPath); } else if (_isDouble) { - retval = double.tryParse(_argResults[argName]) as T; - if (retval == null) _throwErrorForTypes(_argResults[argName]); + var value = double.tryParse(_argResults[argName]); + if (value == null) _throwErrorForTypes(_argResults[argName]); + return _OptionValueWithContext(value as T, _directoryCurrentPath); } else if (_isMapString) { - retval = {} as T; + var value = {}; for (String pair in _argResults[argName]) { var pairList = pair.split('::'); if (pairList.length != 2) { _throwErrorForTypes(pair); } - assert(pairList.length == 2); - (retval as Map)[pairList.first] = pairList.last; + value[pairList.first] = pairList.last; } + return _OptionValueWithContext(value as T, _directoryCurrentPath); } else { throw UnsupportedError('Type $T is not supported'); } - return _OptionValueWithContext(retval, _directoryCurrentPath); } /// The name of this option as a command line argument. @@ -1150,7 +1116,8 @@ abstract class _DartdocArgOption implements DartdocOption { final camelCaseRegexp = RegExp(r'([a-z])([A-Z])(?=([a-z]))'); argName = argName.replaceAllMapped(camelCaseRegexp, (Match m) { var before = m.group(1); - var after = m.group(2).toLowerCase(); + // Group 2 is not optional. + var after = m.group(2)!.toLowerCase(); return '$before-$after'; }); return argName; @@ -1160,14 +1127,14 @@ abstract class _DartdocArgOption implements DartdocOption { /// [ArgParser.addFlag], [ArgParser.addOption], or [ArgParser.addMultiOption] /// as appropriate for [T]. @override - void _onAdd() { + void _addToArgParser(ArgParser argParser) { if (_isBool) { argParser.addFlag(argName, abbr: abbr, - defaultsTo: defaultsTo as bool, + defaultsTo: defaultsTo as bool?, help: help, hide: hide, - negatable: negatable ?? false); + negatable: negatable); } else if (_isInt || _isDouble || _isString) { argParser.addOption(argName, abbr: abbr, @@ -1175,20 +1142,29 @@ abstract class _DartdocArgOption implements DartdocOption { help: help, hide: hide); } else if (_isListString || _isMapString) { - var defaultsToList = []; - if (_isListString) { - defaultsToList = defaultsTo as List; + if (defaultsTo == null) { + argParser.addMultiOption(argName, + abbr: abbr, + defaultsTo: null, + help: help, + hide: hide, + splitCommas: splitCommas); } else { - defaultsToList.addAll((defaultsTo as Map) - .entries - .map((m) => '${m.key}::${m.value}')); + var defaultsToList = []; + if (_isListString) { + defaultsToList = defaultsTo as List; + } else { + defaultsToList.addAll((defaultsTo as Map) + .entries + .map((m) => '${m.key}::${m.value}')); + } + argParser.addMultiOption(argName, + abbr: abbr, + defaultsTo: defaultsToList, + help: help, + hide: hide, + splitCommas: splitCommas); } - argParser.addMultiOption(argName, - abbr: abbr, - defaultsTo: defaultsToList, - help: help, - hide: hide, - splitCommas: splitCommas); } else { throw UnsupportedError('Type $T is not supported'); } @@ -1217,29 +1193,25 @@ class DartdocOptionContext extends DartdocOptionContextBase @override final DartdocOptionSet optionSet; @override - Folder context; + final Folder context; // TODO(jcollins-g): Allow passing in structured data to initialize a // [DartdocOptionContext]'s arguments instead of having to parse strings // via optionSet. DartdocOptionContext(this.optionSet, Resource contextLocation, - ResourceProvider resourceProvider) { - assert(contextLocation != null); - context = resourceProvider.getFolder(resourceProvider.pathContext - .canonicalize(contextLocation is File - ? contextLocation.parent2.path - : contextLocation.path)); - } + ResourceProvider resourceProvider) + : context = resourceProvider.getFolder(resourceProvider.pathContext + .canonicalize(contextLocation is File + ? contextLocation.parent2.path + : contextLocation.path)); /// Build a DartdocOptionContext via the 'inputDir' command line option. DartdocOptionContext.fromDefaultContextLocation( - this.optionSet, ResourceProvider resourceProvider) { - var current = resourceProvider.pathContext.current; - String inputDir = - optionSet['inputDir'].valueAt(resourceProvider.getFolder(current)) ?? - current; - context = resourceProvider.getFolder(inputDir); - } + this.optionSet, ResourceProvider resourceProvider) + : context = resourceProvider.getFolder(optionSet['inputDir'].valueAt( + resourceProvider + .getFolder(resourceProvider.pathContext.current)) ?? + resourceProvider.pathContext.current); /// Build a DartdocOptionContext from an analyzer element (using its source /// location). @@ -1284,21 +1256,17 @@ class DartdocOptionContext extends DartdocOptionContextBase List get dropTextFrom => optionSet['dropTextFrom'].valueAt(context); - String get examplePathPrefix => + String? get examplePathPrefix => optionSet['examplePathPrefix'].valueAt(context); - /// A memoized calculation of exclusions. // TODO(srawlins): This memoization saved a lot of time in unit testing, but // is the first value in this class to be memoized. Memoize others? - /*late final*/ List _exclude; - - List get exclude => - _exclude ??= optionSet['exclude'].valueAt(context); + late final List exclude = optionSet['exclude'].valueAt(context); List get excludePackages => optionSet['excludePackages'].valueAt(context); - String get flutterRoot => optionSet['flutterRoot'].valueAt(context); + String? get flutterRoot => optionSet['flutterRoot'].valueAt(context); bool get hideSdkText => optionSet['hideSdkText'].valueAt(context); @@ -1392,6 +1360,7 @@ Future> createDartdocOptions( 'in the current package or "include-external"', negatable: true), DartdocOptionArgFile>('categoryOrder', [], resourceProvider, + splitCommas: true, help: 'A list of categories (not package names) to place first when ' "grouping symbols on dartdoc's sidebar. Unmentioned categories are " 'sorted after these.'), @@ -1425,7 +1394,7 @@ Future> createDartdocOptions( return []; }, resourceProvider, help: 'Remove text from libraries with the following names.'), - DartdocOptionArgFile('examplePathPrefix', null, resourceProvider, + DartdocOptionArgFile('examplePathPrefix', null, resourceProvider, optionIs: OptionKind.dir, help: 'Prefix for @example paths.\n(defaults to the project root)', mustExist: true), @@ -1433,12 +1402,12 @@ Future> createDartdocOptions( help: 'Library names to ignore.', splitCommas: true), DartdocOptionArgOnly>('excludePackages', [], resourceProvider, help: 'Package names to ignore.', splitCommas: true), - DartdocOptionSyntheticOnly( - 'flutterRoot', - (DartdocSyntheticOption option, Folder dir) => resourceProvider - .pathContext - .resolveTildePath(Platform.environment['FLUTTER_ROOT']), - resourceProvider, + DartdocOptionSyntheticOnly('flutterRoot', + (DartdocSyntheticOption option, Folder dir) { + var envFlutterRoot = Platform.environment['FLUTTER_ROOT']; + if (envFlutterRoot == null) return null; + return resourceProvider.pathContext.resolveTildePath(envFlutterRoot); + }, resourceProvider, optionIs: OptionKind.dir, help: 'Root of the Flutter SDK, specified from environment.', mustExist: true), @@ -1449,8 +1418,7 @@ Future> createDartdocOptions( negatable: true), DartdocOptionArgFile>('include', [], resourceProvider, help: 'Library names to generate docs for.', splitCommas: true), - DartdocOptionArgFile>( - 'includeExternal', null, resourceProvider, + DartdocOptionArgFile>('includeExternal', [], resourceProvider, optionIs: OptionKind.file, help: 'Additional (external) dart files to include; use "dir/fileName", ' @@ -1504,14 +1472,20 @@ Future> createDartdocOptions( // Prefer SDK check first, then pub cache check. var inSdk = packageMeta .sdkType(option.parent.parent['flutterRoot'].valueAt(dir)); + // Analyzer may be confused because package_meta still needs + // migrating. It can definitely return null. + // ignore: unnecessary_null_comparison if (inSdk != null) { Map sdks = option.parent['sdks'].valueAt(dir); - if (sdks.containsKey(inSdk)) return sdks[inSdk]; + var inSdkVal = sdks[inSdk]; + if (inSdkVal != null) return inSdkVal; } var hostedAt = packageMeta.hostedAt; + // ignore: unnecessary_null_comparison if (hostedAt != null) { Map hostMap = option.parent['hosted'].valueAt(dir); - if (hostMap.containsKey(hostedAt)) return hostMap[hostedAt]; + var hostedAtVal = hostMap[hostedAt]; + if (hostedAtVal != null) return hostedAtVal; } return ''; }, resourceProvider, help: 'Url to use for this particular package.'), @@ -1531,6 +1505,7 @@ Future> createDartdocOptions( 'packageMeta', (DartdocSyntheticOption option, Folder dir) { var packageMeta = packageMetaProvider.fromDir(dir); + // ignore: unnecessary_null_comparison if (packageMeta == null) { throw DartdocOptionError( 'Unable to determine package for directory: ${dir.path}'); @@ -1540,19 +1515,20 @@ Future> createDartdocOptions( resourceProvider, ), DartdocOptionArgOnly>('packageOrder', [], resourceProvider, + splitCommas: true, help: 'A list of package names to place first when grouping libraries in ' 'packages. Unmentioned packages are sorted after these.'), - DartdocOptionArgOnly('resourcesDir', null, resourceProvider, + DartdocOptionArgOnly('resourcesDir', null, resourceProvider, help: "An absolute path to dartdoc's resources directory.", hide: true), DartdocOptionArgOnly('sdkDocs', false, resourceProvider, help: 'Generate ONLY the docs for the Dart SDK.'), - DartdocOptionArgSynth('sdkDir', - (DartdocSyntheticOption option, Folder dir) { + DartdocOptionArgSynth('sdkDir', + (DartdocSyntheticOption option, Folder dir) { if (!(option.parent['sdkDocs'].valueAt(dir) as bool) && (option.root['topLevelPackageMeta'].valueAt(dir) as PackageMeta) .requiresFlutter) { - String flutterRoot = option.root['flutterRoot'].valueAt(dir); + String? flutterRoot = option.root['flutterRoot'].valueAt(dir); if (flutterRoot == null) { // For now, return null. An error is reported in // [PackageBuilder.buildPackageGraph]. @@ -1573,6 +1549,7 @@ Future> createDartdocOptions( (DartdocSyntheticOption option, Folder dir) { var packageMeta = packageMetaProvider.fromDir( resourceProvider.getFolder(option.parent['inputDir'].valueAt(dir))); + // ignore: unnecessary_null_comparison if (packageMeta == null) { throw DartdocOptionError( 'Unable to generate documentation: no package found'); diff --git a/lib/src/element_type.dart b/lib/src/element_type.dart index e7b6e948da..0483c215ee 100644 --- a/lib/src/element_type.dart +++ b/lib/src/element_type.dart @@ -12,13 +12,14 @@ import 'package:dartdoc/src/model/comment_referable.dart'; import 'package:dartdoc/src/model/model.dart'; import 'package:dartdoc/src/model/model_object_builder.dart'; import 'package:dartdoc/src/render/element_type_renderer.dart'; +import 'package:meta/meta.dart'; mixin ElementTypeBuilderImpl implements ElementTypeBuilder { PackageGraph get packageGraph; @override ElementType typeFrom(DartType f, Library library, - {ElementType returnedFrom}) => + {ElementType? returnedFrom}) => ElementType._from(f, library, packageGraph, returnedFrom: returnedFrom); } @@ -26,21 +27,23 @@ mixin ElementTypeBuilderImpl implements ElementTypeBuilder { /// may link to a [ModelElement]. abstract class ElementType extends Privacy with CommentReferable, Nameable, ModelBuilder { - final DartType _type; + final DartType type; @override final PackageGraph packageGraph; - final ElementType returnedFrom; + final ElementType? returnedFrom; @override final Library library; - ElementType(this._type, this.library, this.packageGraph, this.returnedFrom); + ElementType(this.type, this.library, this.packageGraph, this.returnedFrom); factory ElementType._from( DartType f, Library library, PackageGraph packageGraph, - {ElementType returnedFrom}) { - if (f.element == null || - f.element.kind == ElementKind.DYNAMIC || - f.element.kind == ElementKind.NEVER) { + {ElementType? returnedFrom}) { + var fElement = f.element; + if (fElement == null || + fElement.kind == ElementKind.DYNAMIC || + fElement.kind == ElementKind.NEVER) { + // [UndefinedElementType]s. if (f is FunctionType) { if (f.alias?.element != null) { return AliasedFunctionTypeElementType( @@ -49,43 +52,42 @@ abstract class ElementType extends Privacy return FunctionTypeElementType(f, library, packageGraph, returnedFrom); } return UndefinedElementType(f, library, packageGraph, returnedFrom); - } else { - var element = packageGraph.modelBuilder.fromElement(f.element); - // [TypeAliasElement.aliasElement] has different implications. - // In that case it is an actual type alias of some kind (generic - // or otherwise. Here however aliasElement signals that this is a - // type referring to an alias. - if (f is! TypeAliasElement && f.alias?.element != null) { - return AliasedElementType( - f, library, packageGraph, element, returnedFrom); - } - assert(f is ParameterizedType || f is TypeParameterType); - // TODO(jcollins-g): strip out all the cruft that's accumulated - // here for non-generic type aliases. - var isGenericTypeAlias = f.alias?.element != null && f is! InterfaceType; - if (f is FunctionType) { - assert(f is ParameterizedType); - // This is an indication we have an extremely out of date analyzer.... - assert( - !isGenericTypeAlias, 'should never occur: out of date analyzer?'); - // And finally, delete this case and its associated class - // after https://dart-review.googlesource.com/c/sdk/+/201520 - // is in all published versions of analyzer this version of dartdoc - // is compatible with. - return CallableElementType( - f, library, packageGraph, element, returnedFrom); - } else if (isGenericTypeAlias) { - return GenericTypeAliasElementType( - f, library, packageGraph, element, returnedFrom); - } - if (f is TypeParameterType) { - return TypeParameterElementType( - f, library, packageGraph, element, returnedFrom); - } + } + // [DefinedElementType]s. + var element = packageGraph.modelBuilder.fromElement(fElement); + // [TypeAliasElement.aliasElement] has different implications. + // In that case it is an actual type alias of some kind (generic + // or otherwise. Here however aliasElement signals that this is a + // type referring to an alias. + if (f is! TypeAliasElement && f.alias?.element != null) { + return AliasedElementType( + f as ParameterizedType, library, packageGraph, element, returnedFrom); + } + assert(f is ParameterizedType || f is TypeParameterType); + // TODO(jcollins-g): strip out all the cruft that's accumulated + // here for non-generic type aliases. + var isGenericTypeAlias = f.alias?.element != null && f is! InterfaceType; + if (f is FunctionType) { assert(f is ParameterizedType); - return ParameterizedElementType( + // This is an indication we have an extremely out of date analyzer.... + assert(!isGenericTypeAlias, 'should never occur: out of date analyzer?'); + // And finally, delete this case and its associated class + // after https://dart-review.googlesource.com/c/sdk/+/201520 + // is in all published versions of analyzer this version of dartdoc + // is compatible with. + return CallableElementType( + f, library, packageGraph, element, returnedFrom); + } else if (isGenericTypeAlias) { + return GenericTypeAliasElementType( + f as TypeParameterType, library, packageGraph, element, returnedFrom); + } + if (f is TypeParameterType) { + return TypeParameterElementType( f, library, packageGraph, element, returnedFrom); } + assert(f is ParameterizedType); + return ParameterizedElementType( + f as ParameterizedType, library, packageGraph, element, returnedFrom); } bool get canHaveParameters => false; @@ -120,29 +122,24 @@ abstract class ElementType extends Privacy @override String toString() => '$type'; - - DartType get type => _type; } /// An [ElementType] that isn't pinned to an Element (or one that is, but whose /// element is irrelevant). class UndefinedElementType extends ElementType { UndefinedElementType(DartType f, Library library, PackageGraph packageGraph, - ElementType returnedFrom) + ElementType? returnedFrom) : super(f, library, packageGraph, returnedFrom); - @override - Element get element => null; - @override bool get isPublic => true; @override String get name { if (type.isVoid) return 'void'; - assert({'Never', 'void', 'dynamic'}.contains(type.element.name), + assert({'Never', 'void', 'dynamic'}.contains(type.element!.name), 'Unrecognized type for UndefinedElementType: ${type.toString()}'); - return type.element.name; + return type.element!.name!; } @override @@ -171,14 +168,14 @@ class UndefinedElementType extends ElementType { Iterable get referenceParents => []; @override - Iterable get referenceGrandparentOverrides => null; + Iterable? get referenceGrandparentOverrides => null; } /// A FunctionType that does not have an underpinning Element. class FunctionTypeElementType extends UndefinedElementType with Rendered, Callable { FunctionTypeElementType(FunctionType f, Library library, - PackageGraph packageGraph, ElementType returnedFrom) + PackageGraph packageGraph, ElementType? returnedFrom) : super(f, library, packageGraph, returnedFrom); /// An unmodifiable list of this function element's type parameters. @@ -197,7 +194,7 @@ class FunctionTypeElementType extends UndefinedElementType class AliasedFunctionTypeElementType extends FunctionTypeElementType with Aliased { AliasedFunctionTypeElementType(FunctionType f, Library library, - PackageGraph packageGraph, ElementType returnedFrom) + PackageGraph packageGraph, ElementType? returnedFrom) : super(f, library, packageGraph, returnedFrom) { assert(type.alias?.element != null); assert(type.alias?.typeArguments != null); @@ -209,47 +206,52 @@ class AliasedFunctionTypeElementType extends FunctionTypeElementType } class ParameterizedElementType extends DefinedElementType with Rendered { - ParameterizedElementType(ParameterizedType type, Library library, - PackageGraph packageGraph, ModelElement element, ElementType returnedFrom) + ParameterizedElementType( + ParameterizedType type, + Library library, + PackageGraph packageGraph, + ModelElement element, + ElementType? returnedFrom) : super(type, library, packageGraph, element, returnedFrom); @override - ParameterizedType get type => super.type; + ParameterizedType get type => super.type as ParameterizedType; @override ElementTypeRenderer get _renderer => packageGraph.rendererFactory.parameterizedElementTypeRenderer; - Iterable _typeArguments; @override - Iterable get typeArguments => - _typeArguments ??= type.typeArguments - .map((f) => modelBuilder.typeFrom(f, library)) - .toList(growable: false); + late final Iterable typeArguments = type.typeArguments + .map((f) => modelBuilder.typeFrom(f, library)) + .toList(growable: false); } -/// A [ElementType] whose underlying type was referrred to by a type alias. +/// A [ElementType] whose underlying type was referred to by a type alias. mixin Aliased implements ElementType, ModelBuilderInterface { + late final Element typeAliasElement = type.alias!.element; + @override - String get name => type.alias.element.name; + String get name => typeAliasElement.name!; @override bool get isTypedef => true; - ModelElement _aliasElement; - ModelElement get aliasElement => - _aliasElement ??= modelBuilder.fromElement(type.alias.element); + late final ModelElement aliasElement = + modelBuilder.fromElement(typeAliasElement); - Iterable _aliasArguments; - Iterable get aliasArguments => - _aliasArguments ??= type.alias.typeArguments - .map((f) => modelBuilder.typeFrom(f, library)) - .toList(growable: false); + late final Iterable aliasArguments = type.alias!.typeArguments + .map((f) => modelBuilder.typeFrom(f, library)) + .toList(growable: false); } class AliasedElementType extends ParameterizedElementType with Aliased { - AliasedElementType(ParameterizedType type, Library library, - PackageGraph packageGraph, ModelElement element, ElementType returnedFrom) + AliasedElementType( + ParameterizedType type, + Library library, + PackageGraph packageGraph, + ModelElement element, + ElementType? returnedFrom) : super(type, library, packageGraph, element, returnedFrom) { assert(type.alias?.element != null); } @@ -258,7 +260,7 @@ class AliasedElementType extends ParameterizedElementType with Aliased { ParameterizedType get type; /// Parameters, if available, for the underlying typedef. - List get aliasedParameters => + late final List aliasedParameters = modelElement.isCallable ? modelElement.parameters : []; @override @@ -267,12 +269,16 @@ class AliasedElementType extends ParameterizedElementType with Aliased { } class TypeParameterElementType extends DefinedElementType { - TypeParameterElementType(TypeParameterType type, Library library, - PackageGraph packageGraph, ModelElement element, ElementType returnedFrom) + TypeParameterElementType( + TypeParameterType type, + Library library, + PackageGraph packageGraph, + ModelElement element, + ElementType? returnedFrom) : super(type, library, packageGraph, element, returnedFrom); @override - TypeParameterType get type => super.type; + TypeParameterType get type => super.type as TypeParameterType; @override String get linkedName => '$name$nullabilitySuffix'; @@ -286,22 +292,16 @@ class TypeParameterElementType extends DefinedElementType { /// An [ElementType] associated with an [Element]. abstract class DefinedElementType extends ElementType { - final ModelElement _modelElement; + final ModelElement modelElement; DefinedElementType(DartType type, Library library, PackageGraph packageGraph, - this._modelElement, ElementType returnedFrom) + this.modelElement, ElementType? returnedFrom) : super(type, library, packageGraph, returnedFrom); - @override - Element get element => modelElement.element; - - ModelElement get modelElement { - assert(_modelElement != null); - return _modelElement; - } + Element get element => modelElement.element!; @override - String get name => type.element.name; + String get name => type.element!.name!; @override String get fullyQualifiedName => modelElement.fullyQualifiedName; @@ -313,34 +313,29 @@ abstract class DefinedElementType extends ElementType { /// would ordinarily do. @override bool get isPublic { - Container canonicalClass = modelElement.packageGraph + var canonicalClass = modelElement.packageGraph .findCanonicalModelElementFor(modelElement.element) ?? modelElement; - return canonicalClass?.isPublic ?? false; + return canonicalClass.isPublic; } DartType get _bound => type; - DartType _instantiatedType; - /// Return this type, instantiated to bounds if it isn't already. @override - DartType get instantiatedType { - if (_instantiatedType == null) { - if (_bound is InterfaceType && - !(_bound as InterfaceType) - .typeArguments - .every((t) => t is InterfaceType)) { - var typeSystem = library.element.typeSystem; - _instantiatedType = typeSystem.instantiateToBounds2( - classElement: _bound.element as ClassElement, - nullabilitySuffix: _bound.nullabilitySuffix); - } else { - _instantiatedType = _bound; - } + late final DartType instantiatedType = () { + if (_bound is InterfaceType && + !(_bound as InterfaceType) + .typeArguments + .every((t) => t is InterfaceType)) { + var typeSystem = library.element.typeSystem; + return typeSystem.instantiateToBounds2( + classElement: _bound.element as ClassElement, + nullabilitySuffix: _bound.nullabilitySuffix); + } else { + return _bound; } - return _instantiatedType; - } + }(); /// The instantiated to bounds type of this type is a subtype of /// [t]. @@ -377,47 +372,41 @@ abstract class DefinedElementType extends ElementType { modelElement.referenceParents; @override - Iterable get referenceGrandparentOverrides => + Iterable? get referenceGrandparentOverrides => modelElement.referenceGrandparentOverrides; + + @internal + @override + CommentReferable get definingCommentReferable => + modelBuilder.fromElement(element); } /// Any callable ElementType will mix-in this class, whether anonymous or not, /// unless it is an alias reference. -mixin Callable implements ElementType { +mixin Callable on ElementType { List get parameters => type.parameters .map((p) => modelBuilder.from(p, library) as Parameter) .toList(growable: false); - ElementType _returnType; - ElementType get returnType { - _returnType ??= modelBuilder.typeFrom(type.returnType, library); - return _returnType; - } + late final ElementType returnType = + modelBuilder.typeFrom(type.returnType, library); @override // TODO(jcollins-g): mustachio should not require this String get linkedName; @override - FunctionType get type => _type; + FunctionType get type => super.type as FunctionType; } /// This [ElementType] uses an [ElementTypeRenderer] to generate /// some of its parameters. mixin Rendered implements ElementType { - String _linkedName; @override - String get linkedName { - _linkedName ??= _renderer.renderLinkedName(this); - return _linkedName; - } + late final String linkedName = _renderer.renderLinkedName(this); - String _nameWithGenerics; @override - String get nameWithGenerics { - _nameWithGenerics ??= _renderer.renderNameWithGenerics(this); - return _nameWithGenerics; - } + late final String nameWithGenerics = _renderer.renderNameWithGenerics(this); ElementTypeRenderer get _renderer; } @@ -425,29 +414,35 @@ mixin Rendered implements ElementType { /// A callable type that may or may not be backed by a declaration using the generic /// function syntax. class CallableElementType extends DefinedElementType with Rendered, Callable { - CallableElementType(FunctionType t, Library library, - PackageGraph packageGraph, ModelElement element, ElementType returnedFrom) + CallableElementType( + FunctionType t, + Library library, + PackageGraph packageGraph, + ModelElement element, + ElementType? returnedFrom) : super(t, library, packageGraph, element, returnedFrom); @override - String get name => - super.name != null && super.name.isNotEmpty ? super.name : 'Function'; + String get name => super.name.isNotEmpty ? super.name : 'Function'; @override ElementTypeRenderer get _renderer => packageGraph.rendererFactory.callableElementTypeRenderer; - Iterable _typeArguments; @override - Iterable get typeArguments => - _typeArguments ??= (type.alias?.typeArguments ?? []) + late final Iterable typeArguments = + (type.alias?.typeArguments ?? []) .map((f) => modelBuilder.typeFrom(f, library)) .toList(growable: false); } /// A non-callable type backed by a [GenericTypeAliasElement]. class GenericTypeAliasElementType extends TypeParameterElementType { - GenericTypeAliasElementType(TypeParameterType t, Library library, - PackageGraph packageGraph, ModelElement element, ElementType returnedFrom) + GenericTypeAliasElementType( + TypeParameterType t, + Library library, + PackageGraph packageGraph, + ModelElement element, + ElementType? returnedFrom) : super(t, library, packageGraph, element, returnedFrom); } diff --git a/lib/src/experiment_options.dart b/lib/src/experiment_options.dart index 9b992a8433..342e6b3253 100644 --- a/lib/src/experiment_options.dart +++ b/lib/src/experiment_options.dart @@ -29,7 +29,6 @@ Future>> createExperimentOptions( 'enable-experiment', ['non-nullable'], resourceProvider, help: 'Enable or disable listed experiments.\n' + ExperimentStatus.knownFeatures.values - .where((e) => e.documentation != null) .map((e) => ' [no-]${e.enableString}: ${e.documentation} (default: ${e.isEnabledByDefault})') .join('\n')), diff --git a/lib/src/generator/dartdoc_generator_backend.dart b/lib/src/generator/dartdoc_generator_backend.dart index 6b24a5f155..c26b0308e1 100644 --- a/lib/src/generator/dartdoc_generator_backend.dart +++ b/lib/src/generator/dartdoc_generator_backend.dart @@ -17,12 +17,12 @@ import 'package:path/path.dart' as path show Context; /// Configuration options for the Dartdoc's default backend. class DartdocGeneratorBackendOptions implements TemplateOptions { @override - final String relCanonicalPrefix; + final String? relCanonicalPrefix; @override final String toolVersion; - final String favicon; + final String? favicon; final bool prettyIndexJson; @@ -38,7 +38,7 @@ class DartdocGeneratorBackendOptions implements TemplateOptions { @override final String customInnerFooterText; - final String /*?*/ resourcesDir; + final String? resourcesDir; DartdocGeneratorBackendOptions.fromContext( DartdocGeneratorOptionContext context) @@ -51,17 +51,6 @@ class DartdocGeneratorBackendOptions implements TemplateOptions { customFooterContent = context.footer, customInnerFooterText = context.footerText, resourcesDir = context.resourcesDir; - - DartdocGeneratorBackendOptions._defaults() - : relCanonicalPrefix = null, - toolVersion = null, - favicon = null, - prettyIndexJson = false, - useBaseHref = false, - customHeaderContent = '', - customFooterContent = '', - customInnerFooterText = '', - resourcesDir = null; } class SidebarGenerator { @@ -88,10 +77,8 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { final ResourceProvider resourceProvider; final path.Context _pathContext; - DartdocGeneratorBackend(DartdocGeneratorBackendOptions options, - this.templates, this.resourceProvider) - : options = options ?? DartdocGeneratorBackendOptions._defaults(), - sidebarForLibrary = SidebarGenerator(templates.renderSidebarForLibrary), + DartdocGeneratorBackend(this.options, this.templates, this.resourceProvider) + : sidebarForLibrary = SidebarGenerator(templates.renderSidebarForLibrary), sidebarForContainer = SidebarGenerator(templates.renderSidebarForContainer), _pathContext = resourceProvider.pathContext; @@ -102,8 +89,9 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { if (!options.useBaseHref) { content = content.replaceAll(htmlBasePlaceholder, data.htmlBase); } + var element = data.self; writer.write(filename, content, - element: data.self is Warnable ? data.self : null); + element: element is Warnable ? element : null); } @override @@ -133,7 +121,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generatePackage(FileWriter writer, PackageGraph graph, Package package) { - TemplateData data = PackageTemplateData(options, graph, package); + var data = PackageTemplateData(options, graph, package); var content = templates.renderIndex(data); write(writer, package.filePath, data, content); } @@ -141,7 +129,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateCategory( FileWriter writer, PackageGraph packageGraph, Category category) { - TemplateData data = CategoryTemplateData(options, packageGraph, category); + var data = CategoryTemplateData(options, packageGraph, category); var content = templates.renderCategory(data); write(writer, category.filePath, data, content); } @@ -149,7 +137,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateLibrary( FileWriter writer, PackageGraph packageGraph, Library lib) { - TemplateData data = LibraryTemplateData( + var data = LibraryTemplateData( options, packageGraph, lib, sidebarForLibrary.getRenderFor); var content = templates.renderLibrary(data); write(writer, lib.filePath, data, content); @@ -158,7 +146,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateClass( FileWriter writer, PackageGraph packageGraph, Library lib, Class clazz) { - TemplateData data = ClassTemplateData(options, packageGraph, lib, clazz, + var data = ClassTemplateData(options, packageGraph, lib, clazz, sidebarForLibrary.getRenderFor, sidebarForContainer.getRenderFor); var content = templates.renderClass(data); write(writer, clazz.filePath, data, content); @@ -167,13 +155,8 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateExtension(FileWriter writer, PackageGraph packageGraph, Library lib, Extension extension) { - TemplateData data = ExtensionTemplateData( - options, - packageGraph, - lib, - extension, - sidebarForLibrary.getRenderFor, - sidebarForContainer.getRenderFor); + var data = ExtensionTemplateData(options, packageGraph, lib, extension, + sidebarForLibrary.getRenderFor, sidebarForContainer.getRenderFor); var content = templates.renderExtension(data); write(writer, extension.filePath, data, content); } @@ -181,7 +164,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateMixin( FileWriter writer, PackageGraph packageGraph, Library lib, Mixin mixin) { - TemplateData data = MixinTemplateData(options, packageGraph, lib, mixin, + var data = MixinTemplateData(options, packageGraph, lib, mixin, sidebarForLibrary.getRenderFor, sidebarForContainer.getRenderFor); var content = templates.renderMixin(data); write(writer, mixin.filePath, data, content); @@ -190,8 +173,8 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateConstructor(FileWriter writer, PackageGraph packageGraph, Library lib, Class clazz, Constructor constructor) { - TemplateData data = ConstructorTemplateData(options, packageGraph, lib, - clazz, constructor, sidebarForContainer.getRenderFor); + var data = ConstructorTemplateData(options, packageGraph, lib, clazz, + constructor, sidebarForContainer.getRenderFor); var content = templates.renderConstructor(data); write(writer, constructor.filePath, data, content); } @@ -199,7 +182,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateEnum( FileWriter writer, PackageGraph packageGraph, Library lib, Enum eNum) { - TemplateData data = EnumTemplateData(options, packageGraph, lib, eNum, + var data = EnumTemplateData(options, packageGraph, lib, eNum, sidebarForLibrary.getRenderFor, sidebarForContainer.getRenderFor); var content = templates.renderEnum(data); write(writer, eNum.filePath, data, content); @@ -208,7 +191,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateFunction(FileWriter writer, PackageGraph packageGraph, Library lib, ModelFunction function) { - TemplateData data = FunctionTemplateData( + var data = FunctionTemplateData( options, packageGraph, lib, function, sidebarForLibrary.getRenderFor); var content = templates.renderFunction(data); write(writer, function.filePath, data, content); @@ -217,8 +200,8 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateMethod(FileWriter writer, PackageGraph packageGraph, Library lib, Container clazz, Method method) { - TemplateData data = MethodTemplateData(options, packageGraph, lib, clazz, - method, sidebarForContainer.getRenderFor); + var data = MethodTemplateData(options, packageGraph, lib, clazz, method, + sidebarForContainer.getRenderFor); var content = templates.renderMethod(data); write(writer, method.filePath, data, content); } @@ -231,8 +214,8 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateProperty(FileWriter writer, PackageGraph packageGraph, Library lib, Container clazz, Field property) { - TemplateData data = PropertyTemplateData(options, packageGraph, lib, clazz, - property, sidebarForContainer.getRenderFor); + var data = PropertyTemplateData(options, packageGraph, lib, clazz, property, + sidebarForContainer.getRenderFor); var content = templates.renderProperty(data); write(writer, property.filePath, data, content); } @@ -240,7 +223,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateTopLevelProperty(FileWriter writer, PackageGraph packageGraph, Library lib, TopLevelVariable property) { - TemplateData data = TopLevelPropertyTemplateData( + var data = TopLevelPropertyTemplateData( options, packageGraph, lib, property, sidebarForLibrary.getRenderFor); var content = templates.renderTopLevelProperty(data); write(writer, property.filePath, data, content); @@ -254,7 +237,7 @@ abstract class DartdocGeneratorBackend implements GeneratorBackend { @override void generateTypeDef(FileWriter writer, PackageGraph packageGraph, Library lib, Typedef typeDef) { - TemplateData data = TypedefTemplateData( + var data = TypedefTemplateData( options, packageGraph, lib, typeDef, sidebarForLibrary.getRenderFor); var content = templates.renderTypedef(data); write(writer, typeDef.filePath, data, content); diff --git a/lib/src/generator/empty_generator.dart b/lib/src/generator/empty_generator.dart index 42e94de966..c421e5615b 100644 --- a/lib/src/generator/empty_generator.dart +++ b/lib/src/generator/empty_generator.dart @@ -24,7 +24,7 @@ class EmptyGenerator extends Generator { .forEach((m) => logProgress(m.documentationAsHtml)); } } - return null; + return Future.value(null); } } diff --git a/lib/src/generator/generator.dart b/lib/src/generator/generator.dart index 3253d60071..d3d5fabe54 100644 --- a/lib/src/generator/generator.dart +++ b/lib/src/generator/generator.dart @@ -7,7 +7,7 @@ library dartdoc.generator; import 'package:analyzer/file_system/file_system.dart'; import 'package:dartdoc/src/dartdoc_options.dart'; -import 'package:dartdoc/src/model/model.dart' show PackageGraph; +import 'package:dartdoc/src/model/package_graph.dart'; import 'package:dartdoc/src/package_meta.dart'; import 'package:dartdoc/src/warnings.dart'; @@ -20,7 +20,7 @@ abstract class FileWriter { /// Writes [content] to a file at [filePath]. /// /// If a file is to be overwritten, a warning will be reported on [element]. - void write(String filePath, String content, {Warnable element}); + void write(String filePath, String content, {Warnable? element}); /// Writes [content] to a file at [filePath]. /// @@ -43,7 +43,7 @@ abstract class Generator { Future generate(PackageGraph packageGraph, FileWriter writer); } -Future>> createGeneratorOptions( +Future> createGeneratorOptions( PackageMetaProvider packageMetaProvider) async { var resourceProvider = packageMetaProvider.resourceProvider; return [ @@ -71,16 +71,16 @@ Future>> createGeneratorOptions( 'Generates `index.json` with indentation and newlines. The file is ' 'larger, but it\'s also easier to diff.', negatable: false), - DartdocOptionArgFile('favicon', null, resourceProvider, + DartdocOptionArgFile('favicon', null, resourceProvider, optionIs: OptionKind.file, help: 'A path to a favicon for the generated docs.', mustExist: true), - DartdocOptionArgOnly('relCanonicalPrefix', null, resourceProvider, + DartdocOptionArgOnly('relCanonicalPrefix', null, resourceProvider, help: 'If provided, add a rel="canonical" prefixed with provided value. ' 'Consider using if building many versions of the docs for public ' 'SEO; learn more at https://goo.gl/gktN6F.'), - DartdocOptionArgOnly('templatesDir', null, resourceProvider, + DartdocOptionArgOnly('templatesDir', null, resourceProvider, optionIs: OptionKind.dir, mustExist: true, hide: true, diff --git a/lib/src/generator/generator_frontend.dart b/lib/src/generator/generator_frontend.dart index 6bfcf2c769..f956da7e13 100644 --- a/lib/src/generator/generator_frontend.dart +++ b/lib/src/generator/generator_frontend.dart @@ -16,9 +16,11 @@ class GeneratorFrontEnd implements Generator { GeneratorFrontEnd(this._generatorBackend); @override - Future generate(PackageGraph packageGraph, FileWriter writer) async { + Future generate(PackageGraph? packageGraph, FileWriter writer) async { var indexElements = []; - _generateDocs(packageGraph, writer, indexElements); + if (packageGraph != null) { + _generateDocs(packageGraph, writer, indexElements); + } await _generatorBackend.generateAdditionalFiles(writer); var categories = indexElements @@ -33,8 +35,6 @@ class GeneratorFrontEnd implements Generator { /// elements. void _generateDocs(PackageGraph packageGraph, FileWriter writer, List indexAccumulator) { - if (packageGraph == null) return; - _generatorBackend.generatePackage( writer, packageGraph, packageGraph.defaultPackage); diff --git a/lib/src/generator/generator_utils.dart b/lib/src/generator/generator_utils.dart index 122e927605..1be0c621d2 100644 --- a/lib/src/generator/generator_utils.dart +++ b/lib/src/generator/generator_utils.dart @@ -14,9 +14,9 @@ import 'package:dartdoc/src/model/model_element.dart'; /// will likely want the same content for this. String generateCategoryJson(Iterable categories, bool pretty) { // ignore: omit_local_variable_types - final List> indexItems = + final List> indexItems = categories.map((Categorization categorization) { - final data = { + final data = { 'name': categorization.name, 'qualifiedName': categorization.fullyQualifiedName, 'href': categorization.href, @@ -49,7 +49,7 @@ String generateCategoryJson(Iterable categories, bool pretty) { String generateSearchIndexJson( Iterable indexedElements, bool pretty) { final indexItems = indexedElements.map((Indexable indexable) { - final data = { + final data = { 'name': indexable.name, 'qualifiedName': indexable.fullyQualifiedName, 'href': indexable.href, @@ -57,13 +57,13 @@ String generateSearchIndexJson( 'overriddenDepth': indexable.overriddenDepth, }; if (indexable is ModelElement) { - data['packageName'] = indexable.package.name; + data['packageName'] = indexable.package?.name; } if (indexable is EnclosedElement) { final ee = indexable as EnclosedElement; - data['enclosedBy'] = { - 'name': ee.enclosingElement.name, - 'type': ee.enclosingElement.kind + data['enclosedBy'] = { + 'name': ee.enclosingElement!.name, + 'type': ee.enclosingElement!.kind }; data['qualifiedName'] = indexable.fullyQualifiedName; @@ -77,10 +77,12 @@ String generateSearchIndexJson( return encoder.convert(indexItems); } -int _sortElementRepresentations(Map a, Map b) { - final value = compareNatural(a['qualifiedName'], b['qualifiedName']); +int _sortElementRepresentations( + Map a, Map b) { + final value = compareNatural( + a['qualifiedName'] as String, b['qualifiedName'] as String); if (value == 0) { - return compareNatural(a['type'], b['type']); + return compareNatural(a['type'] as String, b['type'] as String); } return value; } diff --git a/lib/src/generator/html_generator.dart b/lib/src/generator/html_generator.dart index f8f482f934..308ab3d941 100644 --- a/lib/src/generator/html_generator.dart +++ b/lib/src/generator/html_generator.dart @@ -39,7 +39,7 @@ class HtmlGeneratorBackend extends DartdocGeneratorBackend { void generatePackage(FileWriter writer, PackageGraph graph, Package package) { super.generatePackage(writer, graph, package); // We have to construct the data again. This only happens once per package. - TemplateData data = PackageTemplateData(options, graph, package); + var data = PackageTemplateData(options, graph, package); var content = templates.renderError(data); write(writer, '__404error.html', data, content); } @@ -47,9 +47,10 @@ class HtmlGeneratorBackend extends DartdocGeneratorBackend { @override Future generateAdditionalFiles(FileWriter writer) async { await _copyResources(writer); - if (options.favicon != null) { + var favicon = options.favicon; + if (favicon != null) { // Allow overwrite of favicon. - var bytes = resourceProvider.getFile(options.favicon).readAsBytesSync(); + var bytes = resourceProvider.getFile(favicon).readAsBytesSync(); writer.writeBytes( _pathJoin('static-assets', 'favicon.png'), bytes, diff --git a/lib/src/generator/markdown_generator.dart b/lib/src/generator/markdown_generator.dart index e51e7b477b..f9a0aa5dc9 100644 --- a/lib/src/generator/markdown_generator.dart +++ b/lib/src/generator/markdown_generator.dart @@ -34,7 +34,7 @@ class MarkdownGeneratorBackend extends DartdocGeneratorBackend { void generatePackage(FileWriter writer, PackageGraph graph, Package package) { super.generatePackage(writer, graph, package); // We have to construct the data again. This only happens once per package. - TemplateData data = PackageTemplateData(options, graph, package); + PackageTemplateData data = PackageTemplateData(options, graph, package); var content = templates.renderError(data); write(writer, '__404error.md', data, content); } diff --git a/lib/src/generator/template_data.dart b/lib/src/generator/template_data.dart index e82681be9a..3d553b621e 100644 --- a/lib/src/generator/template_data.dart +++ b/lib/src/generator/template_data.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:dartdoc/src/model/model.dart'; typedef ContainerSidebar = String Function( @@ -9,7 +10,7 @@ typedef ContainerSidebar = String Function( typedef LibrarySidebar = String Function(Library, TemplateDataWithLibrary); abstract class TemplateOptions { - String get relCanonicalPrefix; + String? get relCanonicalPrefix; String get toolVersion; bool get useBaseHref; String get customHeaderContent; @@ -29,7 +30,7 @@ abstract class TemplateData { List get navLinks; List get navLinksWithGenerics => []; - Documentable get parent { + Documentable? get parent { if (navLinksWithGenerics.isEmpty) { return navLinks.isNotEmpty ? navLinks.last : null; } @@ -40,17 +41,18 @@ abstract class TemplateData { bool get hasHomepage => false; - String get homepage => null; + String? get homepage => null; String get htmlBase; T get self; String get version => htmlOptions.toolVersion; - String get relCanonicalPrefix => htmlOptions.relCanonicalPrefix; + String? get relCanonicalPrefix => htmlOptions.relCanonicalPrefix; bool get useBaseHref => htmlOptions.useBaseHref; String get bareHref { if (self is Indexable) { - return (self as Indexable).href.replaceAll(htmlBasePlaceholder, ''); + var selfHref = (self as Indexable).href ?? ''; + return selfHref.replaceAll(htmlBasePlaceholder, ''); } return ''; } @@ -210,10 +212,10 @@ class ClassTemplateData extends InheritingContainerTemplateData { abstract class InheritingContainerTemplateData extends TemplateData implements TemplateDataWithLibrary, TemplateDataWithContainer { - final InheritingContainer clazz; + final T clazz; @override final Library library; - Class _objectType; + Class? _objectType; final LibrarySidebar _sidebarForLibrary; final ContainerSidebar _sidebarForContainer; @@ -234,8 +236,7 @@ abstract class InheritingContainerTemplateData @override T get self => clazz; - String get linkedObjectType => - objectType == null ? 'Object' : objectType.linkedName; + String get linkedObjectType => objectType?.linkedName ?? 'Object'; @override String get title => '${clazz.name} ${clazz.kind} - ${library.name} library - Dart API'; @@ -252,13 +253,13 @@ abstract class InheritingContainerTemplateData @override String get htmlBase => '../'; - Class get objectType { + Class? get objectType { if (_objectType != null) { - return _objectType; + return _objectType!; } var dc = _packageGraph.libraries - .firstWhere((it) => it.name == 'dart:core', orElse: () => null); + .firstWhereOrNull((it) => it.name == 'dart:core'); return _objectType = dc?.getClassByName('Object'); } diff --git a/lib/src/generator/templates.aot_renderers_for_html.dart b/lib/src/generator/templates.aot_renderers_for_html.dart index 00a30c0178..7ce439efda 100644 --- a/lib/src/generator/templates.aot_renderers_for_html.dart +++ b/lib/src/generator/templates.aot_renderers_for_html.dart @@ -38,205 +38,185 @@ String renderCategory(_i1.CategoryTemplateData context0) {
'''); var context1 = context0.self; - if (context1 != null) { - buffer.writeln(); - buffer.write(''' + buffer.writeln(); + buffer.write('''

'''); - buffer.writeEscaped(context1.name.toString()); - buffer.write(''' '''); - buffer.writeEscaped(context1.kind.toString()); - buffer.write('''

+ buffer.writeEscaped(context1.name.toString()); + buffer.write(''' '''); + buffer.writeEscaped(context1.kind.toString()); + buffer.write(''' '''); - buffer.write(_renderCategory_partial_documentation_1(context1, context0)); + buffer.write(_renderCategory_partial_documentation_1(context1, context0)); + buffer.writeln(); + if (context1.hasPublicLibraries == true) { buffer.writeln(); - if (context1.hasPublicLibraries == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Libraries

'''); - var context2 = context1.publicLibrariesSorted; - if (context2 != null) { - for (var context3 in context2) { - buffer.write('\n '); - buffer.write( - _renderCategory_partial_library_2(context3, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context2 = context1.publicLibrariesSorted; + for (var context3 in context2) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_library_2(context3, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicClasses == true) { buffer.writeln(); - if (context1.hasPublicClasses == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Classes

'''); - var context4 = context1.publicClassesSorted; - if (context4 != null) { - for (var context5 in context4) { - buffer.write('\n '); - buffer.write(_renderCategory_partial_container_3( - context5, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context4 = context1.publicClassesSorted; + for (var context5 in context4) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_container_3(context5, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicMixins == true) { buffer.writeln(); - if (context1.hasPublicMixins == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Mixins

'''); - var context6 = context1.publicMixinsSorted; - if (context6 != null) { - for (var context7 in context6) { - buffer.write('\n '); - buffer.write(_renderCategory_partial_container_3( - context7, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context6 = context1.publicMixinsSorted; + for (var context7 in context6) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_container_3(context7, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicConstants == true) { buffer.writeln(); - if (context1.hasPublicConstants == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Constants

'''); - var context8 = context1.publicConstantsSorted; - if (context8 != null) { - for (var context9 in context8) { - buffer.write('\n '); - buffer.write( - _renderCategory_partial_constant_4(context9, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context8 = context1.publicConstantsSorted; + for (var context9 in context8) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_constant_4(context9, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicProperties == true) { buffer.writeln(); - if (context1.hasPublicProperties == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Properties

'''); - var context10 = context1.publicPropertiesSorted; - if (context10 != null) { - for (var context11 in context10) { - buffer.write('\n '); - buffer.write(_renderCategory_partial_property_5( - context11, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context10 = context1.publicPropertiesSorted; + for (var context11 in context10) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_property_5(context11, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicFunctions == true) { buffer.writeln(); - if (context1.hasPublicFunctions == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Functions

'''); - var context12 = context1.publicFunctionsSorted; - if (context12 != null) { - for (var context13 in context12) { - buffer.write('\n '); - buffer.write(_renderCategory_partial_callable_6( - context13, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context12 = context1.publicFunctionsSorted; + for (var context13 in context12) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_callable_6(context13, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicEnums == true) { buffer.writeln(); - if (context1.hasPublicEnums == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Enums

'''); - var context14 = context1.publicEnumsSorted; - if (context14 != null) { - for (var context15 in context14) { - buffer.write('\n '); - buffer.write(_renderCategory_partial_container_3( - context15, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context14 = context1.publicEnumsSorted; + for (var context15 in context14) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_container_3(context15, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicTypedefs == true) { buffer.writeln(); - if (context1.hasPublicTypedefs == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Typedefs

'''); - var context16 = context1.publicTypedefsSorted; - if (context16 != null) { - for (var context17 in context16) { - buffer.write('\n '); - buffer.write( - _renderCategory_partial_typedef_7(context17, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context16 = context1.publicTypedefsSorted; + for (var context17 in context16) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_typedef_7(context17, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } + } + buffer.writeln(); + if (context1.hasPublicExceptions == true) { buffer.writeln(); - if (context1.hasPublicExceptions == true) { - buffer.writeln(); - buffer.write(''' + buffer.write('''

Exceptions / Errors

'''); - var context18 = context1.publicExceptionsSorted; - if (context18 != null) { - for (var context19 in context18) { - buffer.write('\n '); - buffer.write(_renderCategory_partial_container_3( - context19, context1, context0)); - } - } - buffer.writeln(); - buffer.write(''' + var context18 = context1.publicExceptionsSorted; + for (var context19 in context18) { + buffer.write('\n '); + buffer.write( + _renderCategory_partial_container_3(context19, context1, context0)); + } + buffer.writeln(); + buffer.write('''
'''); - } } buffer.writeln(); buffer.write(''' @@ -249,9 +229,9 @@ String renderCategory(_i1.CategoryTemplateData context0) { buffer.writeln(); buffer.write('''
'''); - buffer.writeEscaped(context0.parent.name.toString()); + buffer.writeEscaped(context0.parent!.name.toString()); buffer.write(''' '''); - buffer.writeEscaped(context0.parent.kind.toString()); + buffer.writeEscaped(context0.parent!.kind.toString()); buffer.write('''
'''); buffer.write(_renderCategory_partial_packages_9(context0)); @@ -306,7 +286,7 @@ String _renderCategory_partial_head_0(_i1.CategoryTemplateData context0) { buffer.writeln(); buffer.write(''' '''); @@ -314,14 +294,12 @@ String _renderCategory_partial_head_0(_i1.CategoryTemplateData context0) { buffer.writeln(); if (context0.useBaseHref == true) { var context2 = context0.htmlBase; - if (context2 != null) { - buffer.writeln(); - buffer.write(''' + buffer.writeln(); + buffer.write(''' '''); - } + buffer.write(context0.htmlBase.toString()); + buffer.write('''">'''); } buffer.write('\n\n '); buffer.writeln(); @@ -370,33 +348,29 @@ String _renderCategory_partial_head_0(_i1.CategoryTemplateData context0) {
'''); + } + if (context2.isConst == true) { + buffer.write('''const'''); + } + buffer.writeln(); + buffer.write(''' '''); - buffer.write(context2.nameWithGenerics.toString()); - buffer.write('''('''); - if (context2.hasParameters == true) { - buffer.write(context2.linkedParamsLines.toString()); - } - buffer.write(''') + if (context2.isDeprecated == true) { + buffer.write('''deprecated'''); + } + buffer.write('''">'''); + buffer.write(context2.nameWithGenerics.toString()); + buffer.write('''('''); + if (context2.hasParameters == true) { + buffer.write(context2.linkedParamsLines.toString()); + } + buffer.write(''') '''); - buffer - .write(_renderConstructor_partial_documentation_3(context2, context0)); - buffer.write('\n\n '); - buffer.write(_renderConstructor_partial_source_code_4(context2, context0)); - buffer.writeln(); - } + buffer.write(_renderConstructor_partial_documentation_3(context2, context0)); + buffer.write('\n\n '); + buffer.write(_renderConstructor_partial_source_code_4(context2, context0)); + buffer.writeln(); buffer.writeln(); buffer.write(''' @@ -2414,9 +2247,9 @@ String renderConstructor(_i1.ConstructorTemplateData context0) { buffer.writeln(); buffer.write('''
'''); - buffer.writeEscaped(context0.parent.name.toString()); + buffer.writeEscaped(context0.parent!.name.toString()); buffer.write(' '); - buffer.writeEscaped(context0.parent.kind.toString()); + buffer.writeEscaped(context0.parent!.kind.toString()); buffer.write('''
'''); buffer.write(context0.sidebarForContainer.toString()); @@ -2462,7 +2295,7 @@ String _renderConstructor_partial_head_0(_i1.ConstructorTemplateData context0) { buffer.writeln(); buffer.write(''' '''); @@ -2470,14 +2303,12 @@ String _renderConstructor_partial_head_0(_i1.ConstructorTemplateData context0) { buffer.writeln(); if (context0.useBaseHref == true) { var context2 = context0.htmlBase; - if (context2 != null) { - buffer.writeln(); - buffer.write(''' + buffer.writeln(); + buffer.write(''' '''); - } + buffer.write(context0.htmlBase.toString()); + buffer.write('''">'''); } buffer.write('\n\n '); buffer.writeln(); @@ -2525,34 +2356,30 @@ String _renderConstructor_partial_head_0(_i1.ConstructorTemplateData context0) {