| // 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. |
| |
| /// A documentation generator for Dart. |
| library dartdoc; |
| |
| import 'dart:async'; |
| import 'dart:io'; |
| |
| import 'package:analyzer/dart/element/element.dart' show LibraryElement; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/file_system/file_system.dart' as fileSystem; |
| import 'package:analyzer/file_system/physical_file_system.dart'; |
| import 'package:analyzer/source/package_map_resolver.dart'; |
| import 'package:analyzer/source/sdk_ext.dart'; |
| import 'package:analyzer/src/context/builder.dart'; |
| import 'package:analyzer/src/dart/sdk/sdk.dart'; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/java_io.dart'; |
| import 'package:analyzer/src/generated/sdk.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/generated/source_io.dart'; |
| import 'package:html/dom.dart' show Element, Document; |
| import 'package:html/parser.dart' show parse; |
| import 'package:package_config/discovery.dart' as package_config; |
| import 'package:path/path.dart' as path; |
| |
| import 'package:tuple/tuple.dart'; |
| import 'src/config.dart'; |
| import 'src/generator.dart'; |
| import 'src/html/html_generator.dart'; |
| import 'src/io_utils.dart'; |
| import 'src/model.dart'; |
| import 'src/model_utils.dart'; |
| import 'src/package_meta.dart'; |
| |
| export 'src/config.dart'; |
| export 'src/element_type.dart'; |
| export 'src/generator.dart'; |
| export 'src/model.dart'; |
| export 'src/package_meta.dart'; |
| export 'src/sdk.dart'; |
| |
| const String name = 'dartdoc'; |
| // Update when pubspec version changes. |
| const String version = '0.11.1'; |
| |
| final String defaultOutDir = path.join('doc', 'api'); |
| |
| /// Initialize and setup the generators. |
| Future<List<Generator>> initGenerators(String url, String relCanonicalPrefix, |
| {List<String> headerFilePaths, |
| List<String> footerFilePaths, |
| List<String> footerTextFilePaths, |
| String faviconPath, |
| bool useCategories: false, |
| bool prettyIndexJson: false}) async { |
| var options = new HtmlGeneratorOptions( |
| url: url, |
| relCanonicalPrefix: relCanonicalPrefix, |
| toolVersion: version, |
| faviconPath: faviconPath, |
| useCategories: useCategories, |
| prettyIndexJson: prettyIndexJson); |
| |
| return [ |
| await HtmlGenerator.create( |
| options: options, |
| headers: headerFilePaths, |
| footers: footerFilePaths, |
| footerTexts: footerTextFilePaths, |
| ) |
| ]; |
| } |
| |
| Map<String, List<fileSystem.Folder>> _calculatePackageMap( |
| fileSystem.Folder dir) { |
| Map<String, List<fileSystem.Folder>> map = new Map(); |
| var info = package_config.findPackagesFromFile(dir.toUri()); |
| |
| for (String name in info.packages) { |
| Uri uri = info.asMap()[name]; |
| fileSystem.Resource resource = |
| PhysicalResourceProvider.INSTANCE.getResource(uri.toFilePath()); |
| if (resource is fileSystem.Folder) { |
| map[name] = [resource]; |
| } |
| } |
| |
| return map; |
| } |
| |
| /// Generates Dart documentation for all public Dart libraries in the given |
| /// directory. |
| class DartDoc { |
| final Directory rootDir; |
| final Directory sdkDir; |
| final List<Generator> generators; |
| final Directory outputDir; |
| final PackageMeta packageMeta; |
| final List<String> includes; |
| final List<String> includeExternals; |
| final List<String> excludes; |
| final Set<String> writtenFiles = new Set(); |
| |
| // Fires when the self checks make progress. |
| StreamController<String> _onCheckProgress; |
| |
| Stopwatch _stopwatch; |
| |
| DartDoc(this.rootDir, this.excludes, this.sdkDir, this.generators, |
| this.outputDir, this.packageMeta, this.includes, |
| {this.includeExternals: const []}) { |
| _onCheckProgress = new StreamController(sync: true); |
| } |
| |
| Stream<String> get onCheckProgress => _onCheckProgress.stream; |
| |
| /// 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. Any other exception can be throw if there is an |
| /// unexpected failure. |
| Future<DartDocResults> generateDocs() async { |
| _stopwatch = new Stopwatch()..start(); |
| |
| List<String> files = packageMeta.isSdk |
| ? const [] |
| : findFilesToDocumentInPackage(rootDir.path).toList(); |
| |
| // TODO(jcollins-g): seems like most of this belongs in the Package constructor |
| List<LibraryElement> libraries = _parseLibraries(files, includeExternals); |
| |
| if (includes != null && includes.isNotEmpty) { |
| Iterable knownLibraryNames = libraries.map((l) => l.name); |
| Set notFound = |
| new Set.from(includes).difference(new Set.from(knownLibraryNames)); |
| if (notFound.isNotEmpty) { |
| throw 'Did not find: [${notFound.join(', ')}] in ' |
| 'known libraries: [${knownLibraryNames.join(', ')}]'; |
| } |
| libraries.removeWhere((lib) => !includes.contains(lib.name)); |
| } else { |
| // remove excluded libraries |
| excludes.forEach((pattern) { |
| libraries.removeWhere((lib) { |
| return lib.name.startsWith(pattern) || lib.name == pattern; |
| }); |
| }); |
| } |
| |
| PackageWarningOptions warningOptions = new PackageWarningOptions(); |
| // TODO(jcollins-g): explode this into detailed command line options. |
| if (config != null && config.showWarnings) { |
| for (PackageWarning kind in PackageWarning.values) { |
| warningOptions.warn(kind); |
| } |
| } |
| Package package; |
| if (config != null && config.autoIncludeDependencies) { |
| package = Package.withAutoIncludedDependencies( |
| libraries, packageMeta, warningOptions); |
| libraries = package.libraries.map((l) => l.element).toList(); |
| // remove excluded libraries again, in case they are picked up through |
| // dependencies. |
| excludes.forEach((pattern) { |
| libraries.removeWhere((lib) { |
| return lib.name.startsWith(pattern) || lib.name == pattern; |
| }); |
| }); |
| } |
| package = new Package(libraries, packageMeta, warningOptions); |
| |
| print( |
| '\ngenerating docs for libraries ${package.libraries.map((Library l) => l.name).join(', ')}\n'); |
| |
| // Go through docs of every model element in package to prebuild the macros index |
| // TODO(jcollins-g): move index building into a cached-on-demand generation |
| // like most other bits in [Package]. |
| package.allCanonicalModelElements.forEach((m) => m.documentation); |
| |
| // Create the out directory. |
| if (!outputDir.existsSync()) outputDir.createSync(recursive: true); |
| |
| for (var generator in generators) { |
| await generator.generate(package, outputDir); |
| writtenFiles.addAll(generator.writtenFiles.map(path.normalize)); |
| } |
| |
| verifyLinks(package, outputDir.path); |
| |
| double seconds = _stopwatch.elapsedMilliseconds / 1000.0; |
| print( |
| "\nDocumented ${package.libraries.length} librar${package.libraries.length == 1 ? 'y' : 'ies'} " |
| "in ${seconds.toStringAsFixed(1)} seconds."); |
| print( |
| "Finished with: ${package.packageWarningCounter.warningCount} warnings, ${package.packageWarningCounter.errorCount} errors"); |
| |
| if (package.libraries.isEmpty) { |
| throw new DartDocFailure( |
| "dartdoc could not find any libraries to document. Run `pub get` and try again."); |
| } |
| |
| if (package.packageWarningCounter.errorCount > 0) { |
| throw new DartDocFailure("dartdoc encountered errors while processing"); |
| } |
| |
| return new DartDocResults(packageMeta, package, outputDir); |
| } |
| |
| void _warn(Package package, PackageWarning kind, String p, String origin, |
| {String source}) { |
| // Ordinarily this would go in [Package.warn], but we don't actually know what |
| // ModelElement to warn on yet. |
| Locatable referenceElement; |
| Set<Locatable> referenceElements; |
| |
| // Make all paths relative to origin. |
| if (path.isWithin(origin, p)) { |
| p = path.relative(p, from: origin); |
| } |
| if (source != null) { |
| if (path.isWithin(origin, source)) { |
| source = path.relative(source, from: origin); |
| } |
| // Source paths are always relative. |
| referenceElements = package.allHrefs[source]; |
| } else { |
| referenceElements = package.allHrefs[p]; |
| } |
| if (referenceElements != null) { |
| if (referenceElements.any((e) => e.isCanonical)) { |
| referenceElement = referenceElements.firstWhere((e) => e.isCanonical); |
| } else { |
| // If we don't have a canonical element, just pick one. |
| referenceElement = |
| referenceElements.isEmpty ? null : referenceElements.first; |
| } |
| } |
| if (referenceElement == null && source == 'index.html') |
| referenceElement = package; |
| package.warn(referenceElement, kind, p); |
| } |
| |
| void _doOrphanCheck(Package package, String origin, Set<String> visited) { |
| String normalOrigin = path.normalize(origin); |
| String staticAssets = path.joinAll([normalOrigin, 'static-assets', '']); |
| String indexJson = path.joinAll([normalOrigin, 'index.json']); |
| bool foundIndex = false; |
| for (FileSystemEntity f |
| in new Directory(normalOrigin).listSync(recursive: true)) { |
| var fullPath = path.normalize(f.path); |
| if (f is Directory) { |
| continue; |
| } |
| if (fullPath.startsWith(staticAssets)) { |
| continue; |
| } |
| if (fullPath == indexJson) { |
| foundIndex = true; |
| _onCheckProgress.add(fullPath); |
| continue; |
| } |
| if (visited.contains(fullPath)) continue; |
| if (!writtenFiles.contains(fullPath)) { |
| // This isn't a file we wrote (this time); don't claim we did. |
| _warn(package, PackageWarning.unknownFile, fullPath, normalOrigin); |
| } else { |
| _warn(package, PackageWarning.orphanedFile, fullPath, normalOrigin); |
| } |
| _onCheckProgress.add(fullPath); |
| } |
| |
| if (!foundIndex) { |
| _warn(package, 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<Iterable<String>, String> _getStringLinksAndHref(String fullPath) { |
| File file = new File("$fullPath"); |
| if (!file.existsSync()) { |
| return null; |
| } |
| Document doc = parse(file.readAsStringSync()); |
| Element base = doc.querySelector('base'); |
| String baseHref; |
| if (base != null) { |
| baseHref = base.attributes['href']; |
| } |
| List<Element> links = doc.querySelectorAll('a'); |
| List<String> stringLinks = links |
| .map((link) => link.attributes['href']) |
| .where((href) => href != null) |
| .toList(); |
| return new Tuple2(stringLinks, baseHref); |
| } |
| |
| void _doCheck( |
| Package package, String origin, Set<String> visited, String pathToCheck, |
| [String source, String fullPath]) { |
| if (fullPath == null) { |
| fullPath = path.joinAll([origin, pathToCheck]); |
| fullPath = path.normalize(fullPath); |
| } |
| |
| Tuple2 stringLinksAndHref = _getStringLinksAndHref(fullPath); |
| if (stringLinksAndHref == null) { |
| _warn(package, PackageWarning.brokenLink, pathToCheck, |
| path.normalize(origin), |
| source: source); |
| _onCheckProgress.add(pathToCheck); |
| return null; |
| } |
| Iterable<String> stringLinks = stringLinksAndHref.item1; |
| String baseHref = stringLinksAndHref.item2; |
| |
| for (String href in stringLinks) { |
| if (!href.startsWith('http') && !href.contains('#')) { |
| var full; |
| if (baseHref != null) { |
| full = '${path.dirname(pathToCheck)}/$baseHref/$href'; |
| } else { |
| full = '${path.dirname(pathToCheck)}/$href'; |
| } |
| var newPathToCheck = path.normalize(full); |
| String newFullPath = path.joinAll([origin, newPathToCheck]); |
| newFullPath = path.normalize(newFullPath); |
| if (!visited.contains(newFullPath)) { |
| visited.add(newFullPath); |
| _doCheck(package, origin, visited, newPathToCheck, pathToCheck, |
| newFullPath); |
| } |
| } |
| } |
| _onCheckProgress.add(pathToCheck); |
| } |
| |
| Map<String, Set<ModelElement>> _hrefs; |
| |
| /// Don't call this method more than once, and only after you've |
| /// generated all docs for the Package. |
| void verifyLinks(Package package, String origin) { |
| assert(_hrefs == null); |
| _hrefs = package.allHrefs; |
| |
| final Set<String> visited = new Set(); |
| final String start = 'index.html'; |
| visited.add(start); |
| stdout.write('\nvalidating docs'); |
| _doCheck(package, origin, visited, start); |
| _doOrphanCheck(package, origin, visited); |
| } |
| |
| List<LibraryElement> _parseLibraries( |
| List<String> files, List<String> includeExternals) { |
| Set<LibraryElement> libraries = new Set(); |
| DartSdk sdk = new FolderBasedDartSdk(PhysicalResourceProvider.INSTANCE, |
| PhysicalResourceProvider.INSTANCE.getFolder(sdkDir.path)); |
| List<UriResolver> resolvers = []; |
| |
| fileSystem.Folder cwd = |
| PhysicalResourceProvider.INSTANCE.getResource(rootDir.path); |
| Map<String, List<fileSystem.Folder>> packageMap = _calculatePackageMap(cwd); |
| |
| EmbedderSdk embedderSdk; |
| DartUriResolver embedderResolver; |
| if (packageMap != null) { |
| resolvers.add(new SdkExtUriResolver(packageMap)); |
| resolvers.add(new PackageMapUriResolver( |
| PhysicalResourceProvider.INSTANCE, packageMap)); |
| var embedderYamls = new EmbedderYamlLocator(packageMap).embedderYamls; |
| embedderSdk = |
| new EmbedderSdk(PhysicalResourceProvider.INSTANCE, embedderYamls); |
| embedderResolver = new DartUriResolver(embedderSdk); |
| if (embedderSdk.urlMappings.length == 0) { |
| // The embedder uri resolver has no mappings. Use the default Dart SDK |
| // uri resolver. |
| resolvers.add(new DartUriResolver(sdk)); |
| } else { |
| // The embedder uri resolver has mappings, use it instead of the default |
| // Dart SDK uri resolver. |
| resolvers.add(embedderResolver); |
| } |
| } else { |
| resolvers.add(new DartUriResolver(sdk)); |
| } |
| resolvers.add( |
| new fileSystem.ResourceUriResolver(PhysicalResourceProvider.INSTANCE)); |
| |
| SourceFactory sourceFactory = new SourceFactory(resolvers); |
| |
| // TODO(jcollins-g): fix this so it actually obeys analyzer options files. |
| var options = new AnalysisOptionsImpl(); |
| options.enableGenericMethods = true; |
| options.enableAssertInitializer = true; |
| |
| AnalysisEngine.instance.processRequiredPlugins(); |
| |
| AnalysisContext context = AnalysisEngine.instance.createAnalysisContext() |
| ..analysisOptions = options |
| ..sourceFactory = sourceFactory; |
| |
| if (packageMeta.isSdk) { |
| libraries |
| .addAll(new Set()..addAll(getSdkLibrariesToDocument(sdk, context))); |
| } |
| |
| List<Source> sources = []; |
| |
| void processLibrary(String filePath) { |
| String name = filePath; |
| if (name.startsWith(Directory.current.path)) { |
| name = name.substring(Directory.current.path.length); |
| if (name.startsWith(Platform.pathSeparator)) name = name.substring(1); |
| } |
| print('parsing ${name}...'); |
| JavaFile javaFile = new JavaFile(filePath).getAbsoluteFile(); |
| Source source = new FileBasedSource(javaFile); |
| |
| // TODO(jcollins-g): remove the manual reversal using embedderSdk when we |
| // upgrade to analyzer-0.30 (where DartUriResolver implements |
| // restoreAbsolute) |
| Uri uri = embedderSdk?.fromFileUri(source.uri)?.uri; |
| if (uri != null) { |
| source = new FileBasedSource(javaFile, uri); |
| } else { |
| uri = context.sourceFactory.restoreUri(source); |
| if (uri != null) { |
| source = new FileBasedSource(javaFile, uri); |
| } |
| } |
| sources.add(source); |
| if (context.computeKindOf(source) == SourceKind.LIBRARY) { |
| LibraryElement library = context.computeLibraryElement(source); |
| if (!isPrivate(library)) libraries.add(library); |
| } |
| } |
| |
| files.forEach(processLibrary); |
| |
| if ((embedderSdk != null) && (embedderSdk.urlMappings.length > 0)) { |
| embedderSdk.urlMappings.keys.forEach((String dartUri) { |
| Source source = embedderSdk.mapDartUri(dartUri); |
| processLibrary(source.fullName); |
| }); |
| } |
| |
| // Ensure that the analysis engine performs all remaining work. |
| AnalysisResult result = context.performAnalysisTask(); |
| while (result.hasMoreWork) { |
| result = context.performAnalysisTask(); |
| } |
| |
| // Use the includeExternals. |
| for (Source source in context.librarySources) { |
| LibraryElement library = context.computeLibraryElement(source); |
| String libraryName = Library.getLibraryName(library); |
| var fullPath = source.fullName; |
| |
| if (includeExternals.any((string) => fullPath.endsWith(string))) { |
| if (libraries.map(Library.getLibraryName).contains(libraryName)) { |
| continue; |
| } |
| libraries.add(library); |
| } else if (config != null && |
| config.autoIncludeDependencies && |
| libraryName != '') { |
| File searchFile = new File(fullPath); |
| searchFile = |
| new File(path.join(searchFile.parent.path, 'pubspec.yaml')); |
| bool foundLibSrc = false; |
| while (!foundLibSrc && searchFile.parent != null) { |
| if (searchFile.existsSync()) break; |
| List<String> pathParts = path.split(searchFile.parent.path); |
| // This is a pretty intensely hardcoded convention, but there seems to |
| // to be no other way to identify what might be a "top level" library |
| // here. If lib/src is in the path between the file and the pubspec, |
| // assume that this is supposed to be private. |
| if (pathParts.length < 2) break; |
| pathParts = pathParts.sublist(pathParts.length - 2, pathParts.length); |
| foundLibSrc = |
| path.join(pathParts[0], pathParts[1]) == path.join('lib', 'src'); |
| searchFile = new File( |
| path.join(searchFile.parent.parent.path, 'pubspec.yaml')); |
| } |
| if (foundLibSrc) continue; |
| libraries.add(library); |
| } |
| } |
| |
| List<AnalysisErrorInfo> errorInfos = []; |
| |
| for (Source source in sources) { |
| context.computeErrors(source); |
| errorInfos.add(context.getErrors(source)); |
| } |
| |
| List<_Error> errors = errorInfos |
| .expand((AnalysisErrorInfo info) { |
| return info.errors.map((error) => |
| new _Error(error, info.lineInfo, packageMeta.dir.path)); |
| }) |
| .where((_Error error) => error.isError) |
| .toList() |
| ..sort(); |
| |
| double seconds = _stopwatch.elapsedMilliseconds / 1000.0; |
| print("Parsed ${libraries.length} " |
| "file${libraries.length == 1 ? '' : 's'} in " |
| "${seconds.toStringAsFixed(1)} seconds.\n"); |
| |
| if (errors.isNotEmpty) { |
| errors.forEach(print); |
| int len = errors.length; |
| throw new DartDocFailure( |
| "encountered ${len} analysis error${len == 1 ? '' : 's'}"); |
| } |
| |
| return libraries.toList(); |
| } |
| } |
| |
| /// This class is returned if dartdoc fails in an expected way (for instance, if |
| /// there is an analysis error in the library). |
| class DartDocFailure { |
| final String message; |
| |
| DartDocFailure(this.message); |
| |
| @override |
| String toString() => message; |
| } |
| |
| /// The results of a [DartDoc.generateDocs] call. |
| class DartDocResults { |
| final PackageMeta packageMeta; |
| final Package package; |
| final Directory outDir; |
| |
| DartDocResults(this.packageMeta, this.package, this.outDir); |
| } |
| |
| class _Error implements Comparable<_Error> { |
| final AnalysisError error; |
| final LineInfo lineInfo; |
| final String projectPath; |
| |
| _Error(this.error, this.lineInfo, this.projectPath); |
| |
| String get description => '${error.message} at ${location}, line ${line}.'; |
| bool get isError => error.errorCode.errorSeverity == ErrorSeverity.ERROR; |
| int get line => lineInfo.getLocation(error.offset).lineNumber; |
| String get location { |
| String path = error.source.fullName; |
| if (path.startsWith(projectPath)) { |
| path = path.substring(projectPath.length + 1); |
| } |
| return path; |
| } |
| |
| int get severity => error.errorCode.errorSeverity.ordinal; |
| |
| String get severityName => error.errorCode.errorSeverity.displayName; |
| |
| @override |
| int compareTo(_Error other) { |
| if (severity == other.severity) { |
| int cmp = error.source.fullName.compareTo(other.error.source.fullName); |
| return cmp == 0 ? line - other.line : cmp; |
| } else { |
| return other.severity - severity; |
| } |
| } |
| |
| @override |
| String toString() => '[${severityName}] ${description}'; |
| } |