import 'dart:io'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/uri_converter.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/source/line_info.dart'; import 'package:path/path.dart' as path; import 'package:chalkdart/chalk.dart'; import 'package:codebrowser_dart/tag.dart'; String? _refForElement(Element e) { final loc = e.location?.components ?? []; if (loc.isNotEmpty && loc.length > 2) { // skip first 2, its the lib name and path to file containing symbol // [dart:core, dart:core/string_buffer.dart, StringBuffer, write] var ret = loc.skip(2).join("."); if (ret.endsWith('.') && loc.last.isEmpty) { // this is a ctor? seems like it ret += "ctor"; } if (ret.contains('?')) { // this is a class field ref? // remove the '?' becasue the decl of field doesn't have it // only refs of the field have it ret = ret.replaceAll('?', ''); } if (ret.contains('/')) { // TODO check what this is ret = ret.replaceAll('/', ''); } return ret; } return null; } class BrowserAstVisitor extends RecursiveAstVisitor { /// List of html tags. These are used by generator when generating html List tags; /// Path to 'refs' directory final String refsPath; /// relative path of the current unit being processed final String relativeFilePath; /// path to source root final String contextRoot; /// current compilation unit being visited final ResolvedUnitResult currentUnit; /// Map of Map pathToUnitLineInfos; /// The symbols of this unit, both public and private Map unitSymbols = {}; /// get uri converter, used to convert package imports to actual uri UriConverter get _converter => currentUnit.session.uriConverter; /// get line infos of current unit LineInfo get _lineInfo => currentUnit.lineInfo; // color used for printing warnings static final error = chalk.red; BrowserAstVisitor(this.refsPath, this.contextRoot, this.relativeFilePath, this.pathToUnitLineInfos, this.currentUnit, this.tags); void _writeDefAndDoc(String ref, int lineNum, {String? doc}) { try { if (ref.isEmpty) return; String refFilePath = path.join(refsPath, ref); final f = File(refFilePath); String line = "\n"; if (doc != null && doc.isNotEmpty) { line += "$doc\n"; } f.writeAsStringSync(line, mode: FileMode.writeOnlyAppend); } catch (e) { print("Failed to write def/doc for $ref"); } } void _writeUse(String ref, int lineNum, String? ctxRef) { try { if (ref.isEmpty) return; String refFilePath = path.join(refsPath, ref); final f = File(refFilePath); String line = " true, _ => false }; } void _visitDeclaration(Declaration node, String declType) { if (node.declaredElement == null) { return; } final e = node.declaredElement!; bool isPrivate = e.isPrivate; String dataRef = ''; String classes = 'type decl def'; String id = ''; String? ref = _refForElement(e); if (ref != null && ref.isNotEmpty && e.displayName.isNotEmpty) { unitSymbols[ref] = e.displayName; } if (isPrivate) { dataRef = ' data-ref="${e.nameOffset}" title="${e.name}" data-type="$declType"'; classes += ' local'; id = 'id="${e.nameOffset}"'; } else { if (ref != null) { dataRef = ' data-ref="$ref" title="${e.name}" data-type="$declType"'; final loc = _lineInfo.getLocation(e.nameOffset); _writeDefAndDoc(ref, loc.lineNumber, doc: e.documentationComment); } } tags.add(Tag(e.nameOffset, e.nameOffset + e.nameLength, 'dfn', 'class="$classes" $dataRef $id')); } @override void visitEnumConstantDeclaration(EnumConstantDeclaration node) { tags.add(Tag(node.name.offset, node.name.end, 'dfn', 'class="enum"')); super.visitEnumConstantDeclaration(node); } @override void visitEnumDeclaration(EnumDeclaration node) { _visitDeclaration(node, 'enum'); node.visitChildren(this); } @override void visitClassDeclaration(ClassDeclaration node) { _visitDeclaration(node, 'class'); super.visitClassDeclaration(node); } @override void visitMixinDeclaration(MixinDeclaration node) { _visitDeclaration(node, 'mixin'); node.visitChildren(this); } @override void visitVariableDeclaration(VariableDeclaration node) { String clas = 'decl'; bool isPrivate = true; String? ref; if (node.declaredElement?.kind == ElementKind.LOCAL_VARIABLE) { clas += ' local'; } else if (node.declaredElement?.kind == ElementKind.FIELD) { clas += ' field'; isPrivate = node.declaredElement?.isPrivate ?? false; clas += isPrivate ? ' local' : ''; if (!isPrivate) { final e = node.declaredElement!; ref = _refForElement(e); if (ref != null) { final line = _lineInfo.getLocation(node.offset).lineNumber; String? doc; if (node.documentationComment != null) { doc = currentUnit.content.substring( node.documentationComment!.offset, node.documentationComment!.end); } _writeDefAndDoc(ref, line, doc: doc); } } } else if (node.declaredElement?.kind == ElementKind.TOP_LEVEL_VARIABLE) { if (node.declaredElement!.isPrivate) { isPrivate = true; clas += ' local'; } clas += ' tu def'; } else { final loc = _lineInfo.getLocation(node.offset); print(error( "Unhandled visitVariableDeclaration: ${node.name} --- ${node.declaredElement?.hasImplicitType} ${node.declaredElement?.kind} ${loc.toString()}")); } String dataType = isPrivate ? (node.declaredElement?.type.toString() ?? "") : ""; String title = node.name.lexeme; dataType = dataType.isNotEmpty ? 'data-type="$dataType"' : ""; String dataRef = isPrivate ? 'data-ref="${node.offset}"' : (ref != null ? 'data-ref="$ref"' : ''); String id = isPrivate ? 'id="${node.offset}"' : ''; String attributes = '$id class="$clas" $dataType $dataRef title="$title"'; tags.add(Tag(node.beginToken.charOffset, node.beginToken.charEnd, 'dfn', attributes)); super.visitVariableDeclaration(node); } @override void visitComment(Comment node) { String doc = node.isDocumentation ? 'class="doc"' : ''; tags.add(Tag(node.offset, node.end, 'i', doc)); super.visitComment(node); } @override void visitNamedType(NamedType node) { if (!node.name2.isKeyword) { // skip 'void' final e = node.element; String attributes = 'class="type"'; if (e != null && e.isPrivate) { final ref = "${e.nameOffset}"; final title = (e.name?.isEmpty ?? true) ? "" : ' title="${e.name}"'; attributes = 'class="local type ref" href="#$ref" data-ref="$ref"$title'; } else if (e != null) { final title = (e.name?.isEmpty ?? true) ? "" : ' title="${e.name}"'; if (_isPartOfCurrentProject(e)) { String href = _getHrefToElement(e); attributes = '$href class="type ref"'; } if (title.isNotEmpty) { attributes += title; } String? ref = _refForElement(e); if (ref != null && !_shouldSkipType(node.name2.lexeme)) { attributes += ' data-ref="$ref"'; final loc = _lineInfo.getLocation(node.name2.offset); final ctx = node.thisOrAncestorMatching( (n) => n is FunctionDeclaration || n is MethodDeclaration); String? ctxRef; if (ctx != null && ctx.offset != node.offset) { if (ctx is FunctionDeclaration && ctx.declaredElement != null) { ctxRef = _refForElement(ctx.declaredElement!); } else if (ctx is MethodDeclaration && ctx.declaredElement != null) { ctxRef = _refForElement(ctx.declaredElement!); } } _writeUse(ref, loc.lineNumber, ctxRef); } } tags.add(Tag(node.name2.charOffset, node.name2.charEnd, 'a', attributes)); } super.visitNamedType(node); } @override void visitDeclaredIdentifier(DeclaredIdentifier node) { final decl = node.declaredElement; if (decl != null) { String attributes = 'data-type="${decl.type}" class="decl local" data-ref="${node.name.charOffset}"'; tags.add(Tag(node.name.charOffset, node.name.charEnd, 'dfn', attributes)); } else { print( "VDI:-> ${node.name} --- ${node.type} -- ${_lineInfo.getLocation(node.offset)} -- ${node.declaredElement?.type}"); } super.visitDeclaredIdentifier(node); } bool _isPartOfCurrentProject(Element e) { return switch (e.source?.fullName) { (String fileName) => path.isWithin(contextRoot, fileName), null => false }; } String _getHrefToElement(Element staticElement) { final targetPath = staticElement.source!.fullName; if (targetPath == currentUnit.path) { final loc = _lineInfo.getLocation(staticElement.nameOffset).lineNumber; return 'href="#$loc"'; } final targetLineInfos = pathToUnitLineInfos[targetPath]; if (targetLineInfos != null) { final loc = targetLineInfos.getLocation(staticElement.nameOffset).lineNumber; final currentUnitPath = currentUnit.path; String relativePath = path.relative(targetPath, from: path.dirname(currentUnitPath)); return 'href="$relativePath.html#$loc"'; } return ''; } Element? _getWriteElement(AstNode node) { var parent = node.parent; if (parent is AssignmentExpression && parent.leftHandSide == node) { return parent.writeElement; } if (parent is PostfixExpression) { return parent.writeElement; } if (parent is PrefixExpression) { return parent.writeElement; } if (parent is PrefixedIdentifier && parent.identifier == node) { return _getWriteElement(parent); } if (parent is PropertyAccess && parent.propertyName == node) { return _getWriteElement(parent); } return null; } Element? _getWriteOrReadElement(SimpleIdentifier node) { var writeElement = _getWriteElement(node); if (writeElement != null) { return writeElement; } return node.staticElement; } @override void visitSimpleIdentifier(SimpleIdentifier node) { var staticElement = _getWriteOrReadElement(node); if (staticElement == null) { if (node.parent is LibraryIdentifier) { // we don't handle this super.visitSimpleIdentifier(node); return; } if (node.name == 'call' && node.parent is MethodInvocation) { super.visitSimpleIdentifier(node); return; } print(error( "Unexpected null static element for node ${node.name} ${node.parent.runtimeType} at ${currentUnit.path}:${_lineInfo.getLocation(node.offset)}")); super.visitSimpleIdentifier(node); return; } if (staticElement.kind == ElementKind.DYNAMIC) { super.visitSimpleIdentifier(node); return; } String cssClass = switch (staticElement.kind) { ElementKind.FUNCTION || ElementKind.CONSTRUCTOR => 'fn', ElementKind.METHOD => 'member fn', ElementKind.PARAMETER || ElementKind.LOCAL_VARIABLE => 'local', ElementKind.GETTER => 'fn', ElementKind.FIELD => 'field', ElementKind.SETTER => 'fn', ElementKind.PREFIX => 'namespace', ElementKind.CLASS || ElementKind.ENUM || ElementKind.TYPE_PARAMETER => 'type', _ => '', }; bool isLocal = staticElement.kind == ElementKind.PARAMETER || staticElement.kind == ElementKind.LOCAL_VARIABLE; if (cssClass.isEmpty) { print(error( "visitSimpleIdentifier: Unhandled element kind? ${staticElement.kind} ${node.name} at ${currentUnit.path}:${_lineInfo.getLocation(node.offset)}")); } if (staticElement.isPrivate) { cssClass += ' local'; } cssClass += ' ref'; String href = ''; String dataRef = ''; final String title = 'title="${node.name}"'; if (staticElement.isPrivate || isLocal) { String ref = staticElement.nameOffset >= 0 ? "${staticElement.nameOffset}" : ''; if (staticElement.kind == ElementKind.GETTER || staticElement.kind == ElementKind.SETTER) { final nonSynthetic = staticElement.nonSynthetic; if (nonSynthetic.kind == ElementKind.FIELD) { ref = "${nonSynthetic.nameOffset}"; cssClass = cssClass.replaceAll('fn', 'field'); } else if (nonSynthetic is TopLevelVariableElement) { ref = "${nonSynthetic.nameOffset}"; cssClass = cssClass.replaceAll('fn', ''); } } if (ref.isNotEmpty) { href = 'href="#$ref"'; dataRef = 'data-ref="$ref"'; } } else { if (_isPartOfCurrentProject(staticElement)) { if (staticElement.kind == ElementKind.GETTER || staticElement.kind == ElementKind.SETTER) { final nonSynthetic = staticElement.nonSynthetic; if (nonSynthetic.kind == ElementKind.FIELD) { href = _getHrefToElement(nonSynthetic); cssClass = cssClass.replaceAll('fn', 'field'); } } else { href = _getHrefToElement(staticElement); } } String? ref = _refForElement(staticElement); if (ref != null && ref.isNotEmpty) { dataRef = 'data-ref="$ref"'; final loc = _lineInfo.getLocation(node.offset); final ctx = node.thisOrAncestorMatching( (n) => n is FunctionDeclaration || n is MethodDeclaration); String? ctxRef; if (ctx != null && ctx.offset != node.offset) { if (ctx is FunctionDeclaration && ctx.declaredElement != null) { ctxRef = _refForElement(ctx.declaredElement!); } else if (ctx is MethodDeclaration && ctx.declaredElement != null) { ctxRef = _refForElement(ctx.declaredElement!); } } _writeUse(ref, loc.lineNumber, ctxRef); } } String tagName = 'a'; String attributes = '$href $dataRef class="$cssClass" $title'; tags.add(Tag(node.offset, node.end, tagName, attributes)); super.visitSimpleIdentifier(node); } String? _getRelativeImportExportPath(String value) { String filePath = value; String currentUnitDir = path.dirname(currentUnit.path); String exportPath = path.join(currentUnitDir, filePath); if (File(exportPath).existsSync() && path.isWithin(contextRoot, exportPath)) { return path.relative(exportPath, from: currentUnitDir); } else { Uri uri = Uri.parse(value); final pathToFile = _converter.uriToPath(uri); if (pathToFile != null && path.isWithin(contextRoot, pathToFile)) { return path.relative(pathToFile, from: currentUnitDir); } } return null; } @override void visitSimpleStringLiteral(SimpleStringLiteral node) { if (node.parent != null && (node.parent is ExportDirective || node.parent is ImportDirective)) { String? relativePath = _getRelativeImportExportPath(node.value); if (relativePath != null) { String href = 'href="$relativePath.html"'; String attributes = 'class="string" $href'; tags.add(Tag(node.offset, node.end, 'a', attributes)); super.visitSimpleStringLiteral(node); return; } } tags.add(Tag(node.offset, node.end, 'q', '')); super.visitSimpleStringLiteral(node); } @override void visitStringInterpolation(StringInterpolation node) { for (final e in node.elements) { if (e is InterpolationString) { tags.add(Tag(e.offset, e.end, 'q', '')); } } super.visitStringInterpolation(node); } void _visitFunction(ExecutableElement e) { for (var e in e.parameters) { final title = e.name.isEmpty ? "" : ' title="${e.displayName}"'; final dataType = 'data-type="${e.type}"'; String attributes = 'id="${e.nameOffset}" class="local decl" $dataType data-ref="${e.nameOffset}"$title'; tags.add( Tag(e.nameOffset, e.nameOffset + e.nameLength, 'dfn', attributes)); } String? ref = _refForElement(e); if (ref != null && ref.isNotEmpty && e.name.isNotEmpty) { unitSymbols[ref] = e.name; } String dataRef = ''; String id = ''; String local = ''; if (e.isPrivate) { dataRef = 'data-ref="${e.nameOffset}" title="${e.name}" data-type="${e.type}"'; id = 'id="${e.nameOffset}"'; local = ' local'; } else { if (ref != null) { dataRef = 'data-ref="$ref" title="${e.name}" data-type="${e.type}"'; final loc = _lineInfo.getLocation(e.nameOffset); _writeDefAndDoc(ref, loc.lineNumber, doc: e.documentationComment); } } tags.add(Tag(e.nameOffset, e.nameOffset + e.nameLength, 'dfn', 'class="decl def fn$local" $id $dataRef')); } @override void visitFunctionDeclaration(FunctionDeclaration node) { _visitFunction(node.declaredElement!); super.visitFunctionDeclaration(node); } @override void visitMethodDeclaration(MethodDeclaration node) { _visitFunction(node.declaredElement!); super.visitMethodDeclaration(node); } @override void visitConstructorDeclaration(ConstructorDeclaration node) { if (node.declaredElement != null) _visitFunction(node.declaredElement!); super.visitConstructorDeclaration(node); } // BEGIN typedef void visitTypeAlias(TypeAlias node) { tags.add( Tag(node.name.charOffset, node.name.charEnd, 'dfn', 'class="typedef"')); } @override void visitFunctionTypeAlias(FunctionTypeAlias node) { visitTypeAlias(node); super.visitFunctionTypeAlias(node); } @override void visitGenericTypeAlias(GenericTypeAlias node) { visitTypeAlias(node); super.visitGenericTypeAlias(node); } @override void visitClassTypeAlias(ClassTypeAlias node) { visitTypeAlias(node); super.visitClassTypeAlias(node); } // END typedef }