blob: 77344a3fc257ba65aa0ab52e9ccd9bb1091df54f [file] [log] [blame]
// Copyright (c) 2023, 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.
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/doc_comment.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
/// A link to another document found in a Dart file.
class DartDocumentLink {
final String targetPath;
final int offset;
final int length;
DartDocumentLink(this.offset, this.length, this.targetPath);
}
/// A visitor to locate links to other documents in a file.
///
/// Such paths include "See example/a/b.dart" in documentation comments.
class DartDocumentLinkVisitor extends RecursiveAstVisitor<void> {
final ParsedUnitResult unit;
final String filePath;
final ResourceProvider resourceProvider;
final _documentLinks = <DartDocumentLink>[];
/// The directory that contains `examples/api`, `null` if not found.
late final Folder? folderWithExamplesApi = () {
var file = resourceProvider.getFile(filePath);
for (var parent in file.parent.withAncestors) {
var apiFolder = parent
.getChildAssumingFolder('examples')
.getChildAssumingFolder('api');
if (apiFolder.exists) {
return parent;
}
}
return null;
}();
DartDocumentLinkVisitor(this.resourceProvider, this.unit)
: filePath = unit.path;
List<DartDocumentLink> findLinks(AstNode node) {
_documentLinks.clear();
node.accept(this);
return _documentLinks;
}
@override
void visitComment(Comment node) {
super.visitComment(node);
var content = unit.content;
var toolDirectives = node.docDirectives
.where((directive) => directive.type == DocDirectiveType.tool)
.whereType<BlockDocDirective>();
for (var toolDirective in toolDirectives) {
var contentsStart = toolDirective.openingTag.end;
var contentsEnd = toolDirective.closingTag?.offset;
// Skip unclosed tags.
if (contentsEnd == null) {
continue;
}
var strValue = content.substring(contentsStart, contentsEnd);
if (strValue.isEmpty) {
continue;
}
var seeCodeIn = '** See code in ';
var startIndex = strValue.indexOf('${seeCodeIn}examples/api/');
if (startIndex != -1) {
final folderWithExamplesApi = this.folderWithExamplesApi;
if (folderWithExamplesApi == null) {
// Examples directory doesn't exist.
return;
}
startIndex += seeCodeIn.length;
var endIndex = strValue.indexOf('.dart') + 5;
var pathSnippet = strValue.substring(startIndex, endIndex);
// Split on '/' because that's what the comment syntax uses, but
// re-join it using the resource provider to get the right separator
// for the platform.
var examplePath = resourceProvider.pathContext.joinAll([
folderWithExamplesApi.path,
...pathSnippet.split('/'),
]);
var offset = contentsStart + startIndex;
var length = endIndex - startIndex;
_documentLinks.add(DartDocumentLink(offset, length, examplePath));
}
}
}
}