diff --git a/pkg/analysis_server/lib/src/analysis_server.dart b/pkg/analysis_server/lib/src/analysis_server.dart index a9dde60f92e..56ca0de2fdf 100644 --- a/pkg/analysis_server/lib/src/analysis_server.dart +++ b/pkg/analysis_server/lib/src/analysis_server.dart @@ -685,6 +685,7 @@ class ServerContextManagerCallbacks extends ContextManagerCallbacks { _notificationManager.recordAnalysisErrors(NotificationManager.serverId, path, server.doAnalysisError_listFromEngine(result)); } + analysisServer.getDocumentationCacheFor(result)?.cacheFromResult(result); var unit = result.unit; if (unit != null) { if (analysisServer._hasAnalysisServiceSubscription( diff --git a/pkg/analysis_server/lib/src/analysis_server_abstract.dart b/pkg/analysis_server/lib/src/analysis_server_abstract.dart index 09a9fbe8d53..448bc1708de 100644 --- a/pkg/analysis_server/lib/src/analysis_server_abstract.dart +++ b/pkg/analysis_server/lib/src/analysis_server_abstract.dart @@ -15,6 +15,7 @@ import 'package:analysis_server/src/plugin/plugin_manager.dart'; import 'package:analysis_server/src/plugin/plugin_watcher.dart'; import 'package:analysis_server/src/server/crash_reporting_attachments.dart'; import 'package:analysis_server/src/server/diagnostic_server.dart'; +import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart'; import 'package:analysis_server/src/services/correction/namespace.dart'; import 'package:analysis_server/src/services/pub/pub_api.dart'; import 'package:analysis_server/src/services/pub/pub_package_service.dart'; @@ -25,6 +26,7 @@ import 'package:analysis_server/src/utilities/file_string_sink.dart'; import 'package:analysis_server/src/utilities/null_string_sink.dart'; import 'package:analysis_server/src/utilities/request_statistics.dart'; import 'package:analysis_server/src/utilities/tee_string_sink.dart'; +import 'package:analyzer/dart/analysis/analysis_context.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/session.dart'; import 'package:analyzer/dart/ast/ast.dart'; @@ -83,6 +85,10 @@ abstract class AbstractAnalysisServer { DeclarationsTracker? declarationsTracker; DeclarationsTrackerData? declarationsTrackerData; + /// A map from analysis contexts to the documentation cache associated with + /// each context. + Map documentationForContext = {}; + /// The DiagnosticServer for this AnalysisServer. If available, it can be used /// to start an http diagnostics server or return the port for an existing /// server. @@ -217,6 +223,7 @@ abstract class AbstractAnalysisServer { void addContextsToDeclarationsTracker() { declarationsTracker?.discardContexts(); + documentationForContext.clear(); for (var driver in driverMap.values) { declarationsTracker?.addContext(driver.analysisContext!); driver.resetUriResolution(); @@ -275,6 +282,19 @@ abstract class AbstractAnalysisServer { DartdocDirectiveInfo(); } + /// Return the object used to cache the documentation for elements in the + /// context that produced the [result], or `null` if there is no cache for the + /// context. + DocumentationCache? getDocumentationCacheFor(ResolvedUnitResult result) { + var context = result.session.analysisContext; + var tracker = declarationsTracker?.getContext(context); + if (tracker == null) { + return null; + } + return documentationForContext.putIfAbsent( + context, () => DocumentationCache(tracker.dartdocDirectiveInfo)); + } + /// Return a [Future] that completes with the [Element] at the given /// [offset] of the given [file], or with `null` if there is no node at the /// [offset] or the node does not have an element. diff --git a/pkg/analysis_server/lib/src/domain_completion.dart b/pkg/analysis_server/lib/src/domain_completion.dart index 4fe65b80a30..78db63a067d 100644 --- a/pkg/analysis_server/lib/src/domain_completion.dart +++ b/pkg/analysis_server/lib/src/domain_completion.dart @@ -108,7 +108,9 @@ class CompletionDomainHandler extends AbstractRequestHandler { await perf.runAsync(contributorTag, (performance) async { try { suggestions.addAll( - await manager.computeSuggestions(performance, request), + await manager.computeSuggestions(performance, request, + documentationCache: + server.getDocumentationCacheFor(request.result)), ); } on AbortCompletion { suggestions.clear(); diff --git a/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart b/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart index 1ea7a5a203d..aea05531f4b 100644 --- a/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart +++ b/pkg/analysis_server/lib/src/services/completion/dart/completion_manager.dart @@ -9,6 +9,7 @@ import 'package:analysis_server/src/services/completion/completion_core.dart'; import 'package:analysis_server/src/services/completion/completion_performance.dart'; import 'package:analysis_server/src/services/completion/dart/arglist_contributor.dart'; import 'package:analysis_server/src/services/completion/dart/combinator_contributor.dart'; +import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart'; import 'package:analysis_server/src/services/completion/dart/extension_member_contributor.dart'; import 'package:analysis_server/src/services/completion/dart/feature_computer.dart'; import 'package:analysis_server/src/services/completion/dart/field_formal_contributor.dart'; @@ -93,6 +94,7 @@ class DartCompletionManager { bool enableOverrideContributor = true, bool enableUriContributor = true, CompletionPreference? completionPreference, + DocumentationCache? documentationCache, }) async { request.checkAborted(); var pathContext = request.resourceProvider.pathContext; @@ -105,6 +107,7 @@ class DartCompletionManager { request, dartdocDirectiveInfo, completionPreference: completionPreference, + documentationCache: documentationCache, ); // Don't suggest in comments. @@ -297,6 +300,8 @@ class DartCompletionRequestImpl implements DartCompletionRequest { @override final CompletionPreference completionPreference; + final DocumentationCache? documentationCache; + DartCompletionRequestImpl._( this.result, this.resourceProvider, @@ -308,7 +313,8 @@ class DartCompletionRequestImpl implements DartCompletionRequest { this.dartdocDirectiveInfo, this._originalRequest, this.performance, - {CompletionPreference? completionPreference}) + {CompletionPreference? completionPreference, + this.documentationCache}) : featureComputer = FeatureComputer(result.typeSystem, result.typeProvider), completionPreference = @@ -431,7 +437,8 @@ class DartCompletionRequestImpl implements DartCompletionRequest { OperationPerformanceImpl performance, CompletionRequest request, DartdocDirectiveInfo? dartdocDirectiveInfo, - {CompletionPreference? completionPreference}) async { + {CompletionPreference? completionPreference, + DocumentationCache? documentationCache}) async { request.checkAborted(); return performance.run( @@ -453,6 +460,7 @@ class DartCompletionRequestImpl implements DartCompletionRequest { request, (request as CompletionRequestImpl).performance, completionPreference: completionPreference, + documentationCache: documentationCache, ); }, ); diff --git a/pkg/analysis_server/lib/src/services/completion/dart/documentation_cache.dart b/pkg/analysis_server/lib/src/services/completion/dart/documentation_cache.dart new file mode 100644 index 00000000000..57244848b67 --- /dev/null +++ b/pkg/analysis_server/lib/src/services/completion/dart/documentation_cache.dart @@ -0,0 +1,196 @@ +// Copyright (c) 2021, 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:analysis_server/src/computer/computer_hover.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/src/dartdoc/dartdoc_directive_info.dart'; + +/// Cached data about the documentation associated with the elements declared in +/// a single analysis context. +class DocumentationCache { + /// A shared instance for elements that have no documentation. + static final DocumentationWithSummary _emptyDocs = + DocumentationWithSummary(full: '', summary: ''); + + /// The object used to compute the documentation associated with a single + /// element. + final DartdocDirectiveInfo dartdocDirectiveInfo; + + /// The documentation associated with the elements that have been cached. The + /// cache is keyed by the path of the file containing the declaration of the + /// element and the qualified name of the element. + final Map> documentationCache = + {}; + + /// Initialize a newly created cache. + DocumentationCache(this.dartdocDirectiveInfo); + + /// Fill the cache with data from the [result]. + void cacheFromResult(ResolvedUnitResult result) { + var element = result.unit?.declaredElement; + if (element != null) { + _cacheFromElement(element); + for (var library in result.libraryElement.importedLibraries) { + _cacheLibrary(library); + } + } + } + + /// Return the data cached for the given [element], or `null` if there is no + /// cached data. + DocumentationWithSummary? dataFor(Element element) { + var parent = element.enclosingElement; + if (parent == null) { + return null; + } + var key = element.name; + if (key == null) { + return null; + } + if (parent is! CompilationUnitElement) { + var parentName = parent.name; + if (parentName == null) { + return null; + } + key = '$parentName.$key'; + parent = parent.enclosingElement; + } + if (parent is CompilationUnitElement) { + var elementMap = documentationCache[_keyForUnit(parent)]; + return elementMap?[key]; + } + return null; + } + + /// Fill the cache with data from the [compilationUnit]. + void _cacheFromElement(CompilationUnitElement compilationUnit) { + var elementMap = + documentationCache.putIfAbsent(_keyForUnit(compilationUnit), () => {}); + for (var element in compilationUnit.accessors) { + if (!element.isSynthetic) { + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + } + } + for (var element in compilationUnit.enums) { + var parentKey = + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + if (parentKey != null) { + for (var member in element.fields) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + } + for (var element in compilationUnit.extensions) { + var parentKey = + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + if (parentKey != null) { + for (var member in element.accessors) { + if (!element.isSynthetic) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + for (var member in element.fields) { + if (!element.isSynthetic) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + for (var member in element.methods) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + } + for (var element in compilationUnit.functions) { + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + } + for (var element in [...compilationUnit.mixins, ...compilationUnit.types]) { + var parentKey = + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + if (parentKey != null) { + for (var member in element.accessors) { + if (!element.isSynthetic) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + for (var member in element.fields) { + if (!element.isSynthetic) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + for (var member in element.methods) { + elementMap.cacheMember(dartdocDirectiveInfo, parentKey, member); + } + } + } + for (var element in compilationUnit.topLevelVariables) { + if (!element.isSynthetic) { + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + } + } + for (var element in compilationUnit.typeAliases) { + elementMap.cacheTopLevelElement(dartdocDirectiveInfo, element); + } + } + + /// Cache the data for the given [library] and every library exported from it + /// if it hasn't already been cached. + void _cacheLibrary(LibraryElement library) { + if (_hasDataFor(library.definingCompilationUnit)) { + return; + } + for (var unit in library.units) { + _cacheFromElement(unit); + } + for (var exported in library.exportedLibraries) { + _cacheLibrary(exported); + } + } + + /// Return `true` if the cache contains data for the [compilationUnit]. + bool _hasDataFor(CompilationUnitElement compilationUnit) { + return documentationCache.containsKey(_keyForUnit(compilationUnit)); + } + + /// Return the key used in the [documentationCache] for the [compilationUnit]. + String _keyForUnit(CompilationUnitElement compilationUnit) => + compilationUnit.source.fullName; +} + +extension on Map { + /// Cache the data associated with the top-level [element], and return the + /// [key] used for the element. This does not cache any data associated with + /// any other elements, including children of the [element]. + String? cacheTopLevelElement( + DartdocDirectiveInfo dartdocDirectiveInfo, Element element) { + var key = element.name; + if (key == null) { + return null; + } + cacheElement(dartdocDirectiveInfo, key, element); + return key; + } + + /// Cache the data associated with the [member] element given that the key + /// associated with the member's parent is [parentKey]. + void cacheMember(DartdocDirectiveInfo dartdocDirectiveInfo, String parentKey, + Element member) { + var name = member.name; + if (name == null) { + return null; + } + cacheElement(dartdocDirectiveInfo, '$parentKey.$name', member); + } + + /// Cache the data associated with the [element], using the given [key]. + DocumentationWithSummary? cacheElement( + DartdocDirectiveInfo dartdocDirectiveInfo, String key, Element element) { + var documentation = DartUnitHoverComputer.computeDocumentation( + dartdocDirectiveInfo, element, + includeSummary: true); + if (documentation is DocumentationWithSummary) { + return this[key] = documentation; + } + return this[key] = DocumentationCache._emptyDocs; + } +} diff --git a/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart b/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart index 179e8072af2..467c95438aa 100644 --- a/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart +++ b/pkg/analysis_server/lib/src/services/completion/dart/suggestion_builder.dart @@ -10,6 +10,7 @@ import 'package:analysis_server/src/protocol_server.dart' as protocol; import 'package:analysis_server/src/protocol_server.dart' hide Element, ElementKind; import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart'; +import 'package:analysis_server/src/services/completion/dart/completion_manager.dart'; import 'package:analysis_server/src/services/completion/dart/feature_computer.dart'; import 'package:analysis_server/src/services/completion/dart/utilities.dart'; import 'package:analysis_server/src/utilities/extensions/ast.dart'; @@ -1110,6 +1111,16 @@ class SuggestionBuilder { /// If the [element] has a documentation comment, fill the [suggestion]'s /// documentation fields. void _setDocumentation(CompletionSuggestion suggestion, Element element) { + final request = this.request; + if (request is DartCompletionRequestImpl) { + var documentationCache = request.documentationCache; + var data = documentationCache?.dataFor(element); + if (data != null) { + suggestion.docComplete = data.full; + suggestion.docSummary = data.summary; + return; + } + } var doc = DartUnitHoverComputer.computeDocumentation( request.dartdocDirectiveInfo, element, includeSummary: true); diff --git a/pkg/analysis_server/tool/code_completion/completion_metrics.dart b/pkg/analysis_server/tool/code_completion/completion_metrics.dart index 434a3c136fa..069482aa01c 100644 --- a/pkg/analysis_server/tool/code_completion/completion_metrics.dart +++ b/pkg/analysis_server/tool/code_completion/completion_metrics.dart @@ -13,6 +13,7 @@ import 'package:analysis_server/src/protocol_server.dart' as protocol; import 'package:analysis_server/src/services/completion/completion_core.dart'; import 'package:analysis_server/src/services/completion/completion_performance.dart'; import 'package:analysis_server/src/services/completion/dart/completion_manager.dart'; +import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart'; import 'package:analysis_server/src/services/completion/dart/feature_computer.dart'; import 'package:analysis_server/src/services/completion/dart/probability_range.dart'; import 'package:analysis_server/src/services/completion/dart/relevance_tables.g.dart'; @@ -1034,6 +1035,8 @@ class CompletionMetricsComputer { MetricsSuggestionListener listener, OperationPerformanceImpl performance, CompletionRequestImpl request, + DartdocDirectiveInfo dartdocDirectiveInfo, + DocumentationCache? documentationCache, [DeclarationsTracker? declarationsTracker, protocol.CompletionAvailableSuggestionsParams? availableSuggestionsParams]) async { @@ -1042,9 +1045,10 @@ class CompletionMetricsComputer { if (declarationsTracker == null) { // available suggestions == false suggestions = await DartCompletionManager( - dartdocDirectiveInfo: DartdocDirectiveInfo(), + dartdocDirectiveInfo: dartdocDirectiveInfo, listener: listener, - ).computeSuggestions(performance, request); + ).computeSuggestions(performance, request, + documentationCache: documentationCache); } else { // available suggestions == true var includedElementKinds = {}; @@ -1053,12 +1057,13 @@ class CompletionMetricsComputer { []; var includedSuggestionSetList = []; suggestions = await DartCompletionManager( - dartdocDirectiveInfo: DartdocDirectiveInfo(), + dartdocDirectiveInfo: dartdocDirectiveInfo, includedElementKinds: includedElementKinds, includedElementNames: includedElementNames, includedSuggestionRelevanceTags: includedSuggestionRelevanceTagList, listener: listener, - ).computeSuggestions(performance, request); + ).computeSuggestions(performance, request, + documentationCache: documentationCache); computeIncludedSetList(declarationsTracker, request.result, includedSuggestionSetList, includedElementNames); @@ -1141,118 +1146,26 @@ class CompletionMetricsComputer { // Loop through each file, resolve the file and call // forEachExpectedCompletion + + var dartdocDirectiveInfo = DartdocDirectiveInfo(); + var documentationCache = DocumentationCache(dartdocDirectiveInfo); + var results = []; var pathContext = context.contextRoot.resourceProvider.pathContext; for (var filePath in context.contextRoot.analyzedFiles()) { if (file_paths.isDart(pathContext, filePath)) { try { - _resolvedUnitResult = await context.currentSession - .getResolvedUnit2(filePath) as ResolvedUnitResult; + var result = await context.currentSession.getResolvedUnit2(filePath) + as ResolvedUnitResult; - var analysisError = getFirstErrorOrNull(_resolvedUnitResult); + var analysisError = getFirstErrorOrNull(result); if (analysisError != null) { print('File $filePath skipped due to errors such as:'); print(' ${analysisError.toString()}'); print(''); continue; - } - - // Use the ExpectedCompletionsVisitor to compute the set of expected - // completions for this CompilationUnit. - final visitor = ExpectedCompletionsVisitor(filePath); - _resolvedUnitResult.unit!.accept(visitor); - - for (var expectedCompletion in visitor.expectedCompletions) { - var resolvedUnitResult = _resolvedUnitResult; - - // If an overlay option is being used, compute the overlay file, and - // have the context reanalyze the file - if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) { - var overlayContents = _getOverlayContents( - _resolvedUnitResult.content!, expectedCompletion); - - _provider.setOverlay(filePath, - content: overlayContents, - modificationStamp: overlayModificationStamp++); - context.driver.changeFile(filePath); - resolvedUnitResult = await context.currentSession - .getResolvedUnit2(filePath) as ResolvedUnitResult; - } - - // As this point the completion suggestions are computed, - // and results are collected with varying settings for - // comparison: - - Future handleExpectedCompletion( - {required MetricsSuggestionListener listener, - required CompletionMetrics metrics}) async { - var stopwatch = Stopwatch()..start(); - var request = CompletionRequestImpl( - resolvedUnitResult, - expectedCompletion.offset, - CompletionPerformance(), - ); - var directiveInfo = DartdocDirectiveInfo(); - - late OpType opType; - late List suggestions; - await request.performance.runRequestOperation( - (performance) async { - var dartRequest = await DartCompletionRequestImpl.from( - performance, request, directiveInfo); - opType = - OpType.forCompletion(dartRequest.target, request.offset); - suggestions = await _computeCompletionSuggestions( - listener, - performance, - request, - metrics.availableSuggestions ? declarationsTracker : null, - metrics.availableSuggestions - ? availableSuggestionsParams - : null, - ); - }, - ); - stopwatch.stop(); - - return forEachExpectedCompletion( - request, - listener, - expectedCompletion, - opType.completionLocation, - suggestions, - metrics, - stopwatch.elapsedMilliseconds); - } - - var bestRank = -1; - var bestName = ''; - var defaultTag = getCurrentTag(); - for (var metrics in targetMetrics) { - // Compute the completions. - metrics.enable(); - metrics.userTag.makeCurrent(); - // if (FeatureComputer.noDisabledFeatures) { - // var line = expectedCompletion.lineNumber; - // var column = expectedCompletion.columnNumber; - // print('$filePath:$line:$column'); - // } - var listener = MetricsSuggestionListener(); - var rank = await handleExpectedCompletion( - listener: listener, metrics: metrics); - if (bestRank < 0 || rank < bestRank) { - bestRank = rank; - bestName = metrics.name; - } - defaultTag.makeCurrent(); - metrics.disable(); - } - rankComparison.count(bestName); - - // If an overlay option is being used, remove the overlay applied - // earlier - if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) { - _provider.removeOverlay(filePath); - } + } else { + results.add(result); + documentationCache.cacheFromResult(result); } } catch (exception, stackTrace) { print('Exception caught analyzing: $filePath'); @@ -1261,6 +1174,104 @@ class CompletionMetricsComputer { } } } + for (var result in results) { + _resolvedUnitResult = result; + var filePath = result.path!; + // Use the ExpectedCompletionsVisitor to compute the set of expected + // completions for this CompilationUnit. + final visitor = ExpectedCompletionsVisitor(filePath); + _resolvedUnitResult.unit!.accept(visitor); + + for (var expectedCompletion in visitor.expectedCompletions) { + var resolvedUnitResult = _resolvedUnitResult; + + // If an overlay option is being used, compute the overlay file, and + // have the context reanalyze the file + if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) { + var overlayContents = _getOverlayContents( + _resolvedUnitResult.content!, expectedCompletion); + + _provider.setOverlay(filePath, + content: overlayContents, + modificationStamp: overlayModificationStamp++); + context.driver.changeFile(filePath); + resolvedUnitResult = await context.currentSession + .getResolvedUnit2(filePath) as ResolvedUnitResult; + } + + // As this point the completion suggestions are computed, + // and results are collected with varying settings for + // comparison: + + Future handleExpectedCompletion( + {required MetricsSuggestionListener listener, + required CompletionMetrics metrics}) async { + var stopwatch = Stopwatch()..start(); + var request = CompletionRequestImpl( + resolvedUnitResult, + expectedCompletion.offset, + CompletionPerformance(), + ); + var directiveInfo = DartdocDirectiveInfo(); + + late OpType opType; + late List suggestions; + await request.performance.runRequestOperation( + (performance) async { + var dartRequest = await DartCompletionRequestImpl.from( + performance, request, directiveInfo); + opType = OpType.forCompletion(dartRequest.target, request.offset); + suggestions = await _computeCompletionSuggestions( + listener, + performance, + request, + dartdocDirectiveInfo, + documentationCache, + metrics.availableSuggestions ? declarationsTracker : null, + metrics.availableSuggestions + ? availableSuggestionsParams + : null, + ); + }, + ); + stopwatch.stop(); + + return forEachExpectedCompletion( + request, + listener, + expectedCompletion, + opType.completionLocation, + suggestions, + metrics, + stopwatch.elapsedMilliseconds); + } + + var bestRank = -1; + var bestName = ''; + var defaultTag = getCurrentTag(); + for (var metrics in targetMetrics) { + // Compute the completions. + metrics.enable(); + metrics.userTag.makeCurrent(); + var listener = MetricsSuggestionListener(); + var rank = await handleExpectedCompletion( + listener: listener, metrics: metrics); + if (bestRank < 0 || rank < bestRank) { + bestRank = rank; + bestName = metrics.name; + } + defaultTag.makeCurrent(); + metrics.disable(); + } + rankComparison.count(bestName); + + // If an overlay option is being used, remove the overlay applied + // earlier. + if (options.overlay != CompletionMetricsOptions.OVERLAY_NONE) { + _provider.removeOverlay(filePath); + } + } + } } List _filterSuggestions( diff --git a/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart b/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart index 540d866cf49..cfcb7326cad 100644 --- a/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart +++ b/pkg/analyzer/lib/src/dartdoc/dartdoc_directive_info.dart @@ -177,7 +177,7 @@ class DartdocDirectiveInfo { } start = eolIndex + 1; } - if (lastNonEmpty < firstNonEmpty) { + if (firstNonEmpty < 0 || lastNonEmpty < firstNonEmpty) { // All of the lines are empty. return const []; }