diff --git a/lib/src/model/model_node.dart b/lib/src/model/model_node.dart index 4f1dda4151..50d65abd12 100644 --- a/lib/src/model/model_node.dart +++ b/lib/src/model/model_node.dart @@ -11,15 +11,22 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:meta/meta.dart'; /// Stripped down information derived from [AstNode] containing only information -/// needed to resurrect the source code of [element]. +/// needed to resurrect the source code of [_element]. class ModelNode { final Element _element; final AnalysisContext _analysisContext; final int _sourceEnd; final int _sourceOffset; + /// Data about each comment reference found in the doc comment of this node. + final Map? commentReferenceData; + factory ModelNode( - AstNode? sourceNode, Element element, AnalysisContext analysisContext) { + AstNode? sourceNode, + Element element, + AnalysisContext analysisContext, { + required Map? commentReferenceData, + }) { if (sourceNode == null) { return ModelNode._(element, analysisContext, sourceEnd: -1, sourceOffset: -1); @@ -32,14 +39,23 @@ class ModelNode { assert(sourceNode is FieldDeclaration || sourceNode is TopLevelVariableDeclaration); } - return ModelNode._(element, analysisContext, - sourceEnd: sourceNode.end, sourceOffset: sourceNode.offset); + return ModelNode._( + element, + analysisContext, + sourceEnd: sourceNode.end, + sourceOffset: sourceNode.offset, + commentReferenceData: commentReferenceData, + ); } } - ModelNode._(this._element, this._analysisContext, - {required int sourceEnd, required int sourceOffset}) - : _sourceEnd = sourceEnd, + ModelNode._( + this._element, + this._analysisContext, { + required int sourceEnd, + required int sourceOffset, + this.commentReferenceData = const {}, + }) : _sourceEnd = sourceEnd, _sourceOffset = sourceOffset; bool get _isSynthetic => _sourceEnd < 0 || _sourceOffset < 0; @@ -63,6 +79,20 @@ class ModelNode { }(); } +/// Comment reference data from the syntax tree. +/// +/// Comment reference data is not available on the analyzer's Element model, so +/// we store it after resolving libraries in instances of this class. +class CommentReferenceData { + final Element element; + final String name; + final int offset; + final int length; + + CommentReferenceData(this.element, String? name, this.offset, this.length) + : name = name ?? ''; +} + @visibleForTesting extension SourceStringExtensions on String { String substringFromLineStart(int offset, int endOffset) { diff --git a/lib/src/model/package_graph.dart b/lib/src/model/package_graph.dart index c7f631f460..b0d06033c7 100644 --- a/lib/src/model/package_graph.dart +++ b/lib/src/model/package_graph.dart @@ -5,11 +5,12 @@ import 'dart:collection'; import 'package:analyzer/dart/analysis/analysis_context.dart'; -import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/source/source.dart'; // ignore: implementation_imports +import 'package:analyzer/src/dart/ast/ast.dart'; +// ignore: implementation_imports import 'package:analyzer/src/dart/element/inheritance_manager3.dart' show InheritanceManager3; // ignore: implementation_imports @@ -234,6 +235,20 @@ class PackageGraph with CommentReferable, Nameable { // me how, because the data is on AST nodes, not the element model. void gatherModelNodes(DartDocResolvedLibrary resolvedLibrary) { for (var unit in resolvedLibrary.units) { + for (var directive in unit.directives.whereType()) { + // There should be only one library directive. If there are more, there + // is no harm in grabbing ModelNode for each. + var commentReferenceData = directive.documentationComment?.data; + _modelNodes.putIfAbsent( + resolvedLibrary.element, + () => ModelNode( + directive, + resolvedLibrary.element, + analysisContext, + commentReferenceData: commentReferenceData, + )); + } + for (var declaration in unit.declarations) { _populateModelNodeFor(declaration); switch (declaration) { @@ -243,6 +258,9 @@ class PackageGraph with CommentReferable, Nameable { } case EnumDeclaration(): if (declaration.declaredElement?.isPublic ?? false) { + for (var constant in declaration.constants) { + _populateModelNodeFor(constant); + } for (var member in declaration.members) { _populateModelNodeFor(member); } @@ -269,12 +287,21 @@ class PackageGraph with CommentReferable, Nameable { } void _populateModelNodeFor(Declaration declaration) { + var commentReferenceData = declaration.documentationComment?.data; + if (declaration is FieldDeclaration) { var fields = declaration.fields.variables; for (var field in fields) { var element = field.declaredElement!; _modelNodes.putIfAbsent( - element, () => ModelNode(field, element, analysisContext)); + element, + () => ModelNode( + field, + element, + analysisContext, + commentReferenceData: commentReferenceData, + ), + ); } return; } @@ -283,13 +310,27 @@ class PackageGraph with CommentReferable, Nameable { for (var field in fields) { var element = field.declaredElement!; _modelNodes.putIfAbsent( - element, () => ModelNode(field, element, analysisContext)); + element, + () => ModelNode( + field, + element, + analysisContext, + commentReferenceData: commentReferenceData, + ), + ); } return; } var element = declaration.declaredElement!; _modelNodes.putIfAbsent( - element, () => ModelNode(declaration, element, analysisContext)); + element, + () => ModelNode( + declaration, + element, + analysisContext, + commentReferenceData: commentReferenceData, + ), + ); } ModelNode? getModelNodeFor(Element element) => _modelNodes[element]; @@ -1029,3 +1070,41 @@ class InheritableElementsKey { InheritableElementsKey(this.element, this.library); } + +extension on Comment { + /// A mapping of all comment references to their various data. + Map get data { + if (references.isEmpty) return const {}; + + var data = {}; + for (var reference in references) { + var commentReferable = reference.expression; + String name; + Element? staticElement; + if (commentReferable case PropertyAccessImpl(:var propertyName)) { + var target = commentReferable.target; + if (target is! PrefixedIdentifierImpl) continue; + name = '${target.name}.${propertyName.name}'; + staticElement = propertyName.staticElement; + } else if (commentReferable case PrefixedIdentifier(:var identifier)) { + name = commentReferable.name; + staticElement = identifier.staticElement; + } else if (commentReferable case SimpleIdentifier()) { + name = commentReferable.name; + staticElement = commentReferable.staticElement; + } else { + continue; + } + + if (staticElement != null && !data.containsKey(name)) { + data[name] = CommentReferenceData( + staticElement, + name, + commentReferable.offset, + commentReferable.length, + ); + } + } + return data; + } +} diff --git a/test/dartdoc_test_base.dart b/test/dartdoc_test_base.dart index 8b8482ad07..835571eb8c 100644 --- a/test/dartdoc_test_base.dart +++ b/test/dartdoc_test_base.dart @@ -154,4 +154,11 @@ $libraryContent ); return await Dartdoc.fromContext(context, packageBuilder); } + + /// The real offset in a library generated with [bootPackageWithLibrary]. + /// + /// When a library is written via [bootPackageWithLibrary], the test author + /// provides `libraryContent`, which is a snippet of Dart library text. + int realOffsetFor(int offsetInContent) => + '\n\nlibrary $libraryName\n\n'.length + offsetInContent; } diff --git a/test/model_node_test.dart b/test/model_node_test.dart index 77eb56c6fc..a8f86d60ca 100644 --- a/test/model_node_test.dart +++ b/test/model_node_test.dart @@ -2,61 +2,190 @@ // 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:dartdoc/src/model/model_node.dart' show SourceStringExtensions; +import 'package:dartdoc/src/model/model_node.dart'; import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import 'dartdoc_test_base.dart'; +import 'src/utils.dart'; void main() { - group('model_utils stripIndentFromSource', () { - test('no indent', () { - expect( - 'void foo() {\n print(1);\n}\n'.stripIndent, - 'void foo() {\n print(1);\n}\n', - ); - }); - - test('same indent', () { - expect( - ' void foo() {\n print(1);\n }\n'.stripIndent, - 'void foo() {\n print(1);\n}\n', - ); - }); - - test('odd indent', () { - expect( - ' void foo() {\n print(1);\n }\n'.stripIndent, - 'void foo() {\n print(1);\n}\n', - ); - }); + defineReflectiveSuite(() { + defineReflectiveTests(ModelNodeTest); }); +} - group('model_utils stripDartdocCommentsFromSource', () { - test('no comments', () { - expect( - 'void foo() {\n print(1);\n}\n'.stripDocComments, - 'void foo() {\n print(1);\n}\n', - ); - }); - - test('line comments', () { - expect( - '/// foo comment\nvoid foo() {\n print(1);\n}\n'.stripDocComments, - 'void foo() {\n print(1);\n}\n', - ); - }); - - test('block comments 1', () { - expect( - '/** foo comment */\nvoid foo() {\n print(1);\n}\n'.stripDocComments, - 'void foo() {\n print(1);\n}\n', - ); - }); - - test('block comments 2', () { - expect( - '/**\n * foo comment\n */\nvoid foo() {\n print(1);\n}\n' - .stripDocComments, - 'void foo() {\n print(1);\n}\n', - ); - }); - }); +@reflectiveTest +class ModelNodeTest extends DartdocTestBase { + @override + final libraryName = 'model_node'; + + void test_onClass_refersToDartCoreImportedElement() async { + var library = await bootPackageWithLibrary(''' +/// Refers to [int]. +class C {} +'''); + var c = library.classes.named('C'); + expect(c.name, equals('C')); + var commentReferenceData = c.modelNode!.commentReferenceData!; + expect( + commentReferenceData['int'], + isA() + .having((e) => e.name, 'name', 'int') + .having((e) => e.offset, 'offset', realOffsetFor(15)) + .having((e) => e.length, 'length', 3), + ); + } + + void test_onClass_refersToDocImportedElement() async { + var library = await bootPackageWithLibrary(''' +/// @docImport 'dart:async'; +library; + +/// Refers to [FutureOr]. +class C {} +'''); + var c = library.classes.named('C'); + expect(c.name, equals('C')); + var commentReferenceData = c.modelNode!.commentReferenceData!; + expect( + commentReferenceData['FutureOr'], + isA() + .having((e) => e.name, 'name', 'FutureOr') + .having((e) => e.offset, 'offset', realOffsetFor(54)) + .having((e) => e.length, 'length', 8), + ); + } + + void test_onClass_refersToImportedElement_propertyAccess() async { + var library = await bootPackageWithLibrary(''' +import 'dart:async' as async; +/// Refers to [async.Future.value]. +class C {} +'''); + var c = library.classes.named('C'); + expect(c.name, equals('C')); + var commentReferenceData = c.modelNode!.commentReferenceData!; + expect( + commentReferenceData['async.Future.value'], + isA() + .having((e) => e.name, 'name', 'async.Future.value') + .having((e) => e.offset, 'offset', realOffsetFor(45)) + .having((e) => e.length, 'length', 18), + ); + } + + void test_onClass_refersToImportedElement_prefixedIdentifier() async { + var library = await bootPackageWithLibrary(''' +/// Refers to [Future.value]. +class C {} +'''); + var c = library.classes.named('C'); + expect(c.name, equals('C')); + var commentReferenceData = c.modelNode!.commentReferenceData!; + expect( + commentReferenceData['Future.value'], + isA() + .having((e) => e.name, 'name', 'Future.value') + .having((e) => e.offset, 'offset', realOffsetFor(15)) + .having((e) => e.length, 'length', 12), + ); + } + + void test_onInstanceGetter_refersToDartCoreImportedElement() async { + var library = await bootPackageWithLibrary(''' +class C { + /// Refers to [int]. + int get g => 1; +} +'''); + var g = library.classes.named('C').instanceAccessors.named('g'); + expect(g.name, equals('g')); + var commentReferenceData = g.modelNode!.commentReferenceData!; + expect( + commentReferenceData['int'], + isA() + .having((e) => e.name, 'name', 'int') + .having((e) => e.offset, 'offset', realOffsetFor(27)) + .having((e) => e.length, 'length', 3), + ); + } + + void test_onVariableDeclarationList() async { + var library = await bootPackageWithLibrary(''' +/// Refers to [int]. +int a = 1, b = 2; +'''); + var a = library.properties.named('a'); + expect(a.name, equals('a')); + var aData = a.modelNode!.commentReferenceData!; + expect( + aData['int'], + isA() + .having((e) => e.name, 'name', 'int') + .having((e) => e.offset, 'offset', realOffsetFor(15)) + .having((e) => e.length, 'length', 3), + ); + + var b = library.properties.named('b'); + expect(b.name, equals('b')); + var bData = b.modelNode!.commentReferenceData!; + expect( + bData['int'], + isA() + .having((e) => e.name, 'name', 'int') + .having((e) => e.offset, 'offset', realOffsetFor(15)) + .having((e) => e.length, 'length', 3), + ); + } + + void test_stripIndent_noIndent() { + expect( + 'void foo() {\n print(1);\n}\n'.stripIndent, + 'void foo() {\n print(1);\n}\n', + ); + } + + void test_stripIndent_sameIndent() { + expect( + ' void foo() {\n print(1);\n }\n'.stripIndent, + 'void foo() {\n print(1);\n}\n', + ); + } + + void test_stripIndent_oddIndent() { + expect( + ' void foo() {\n print(1);\n }\n'.stripIndent, + 'void foo() {\n print(1);\n}\n', + ); + } + + void test_stripDocComments_noComments() { + expect( + 'void foo() {\n print(1);\n}\n'.stripDocComments, + 'void foo() {\n print(1);\n}\n', + ); + } + + void test_stripDocComments_lineComments() { + expect( + '/// foo comment\nvoid foo() {\n print(1);\n}\n'.stripDocComments, + 'void foo() {\n print(1);\n}\n', + ); + } + + void test_stripDocComments_blockComments1() { + expect( + '/** foo comment */\nvoid foo() {\n print(1);\n}\n'.stripDocComments, + 'void foo() {\n print(1);\n}\n', + ); + } + + void test_stripDocComments_blockComments2() { + expect( + '/**\n * foo comment\n */\nvoid foo() {\n print(1);\n}\n' + .stripDocComments, + 'void foo() {\n print(1);\n}\n', + ); + } }