| // Copyright (c) 2026, 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/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/src/dart/analysis/session.dart'; |
| import 'package:analyzer/src/dart/ast/element_locator.dart'; |
| import 'package:analyzer/src/dart/ast/extensions.dart'; |
| import 'package:analyzer/src/utilities/extensions/collection.dart'; |
| |
| class DartDocumentHighlightsComputer { |
| final CompilationUnit _unit; |
| |
| DartDocumentHighlightsComputer(this._unit); |
| |
| /// Computes matching highlight tokens for the requested offset. |
| List<Token> compute(int requestedOffset) { |
| var coveringNode = _unit.nodeCovering(offset: requestedOffset); |
| coveringNode = _adjustNode(requestedOffset, coveringNode); |
| if (coveringNode == null) return []; |
| |
| var targets = _computeTargets(coveringNode); |
| if (targets == null) return []; |
| |
| var visitor = _DartDocumentHighlightsVisitor(targets); |
| _unit.accept(visitor); |
| return visitor.tokens.toList(); |
| } |
| |
| /// Adjusts the result of `nodeCovering` for cases where a position falls |
| /// between two nodes and the wrong one is selected. |
| AstNode? _adjustNode(int offset, AstNode? coveringNode) { |
| return switch (coveringNode) { |
| // In `ClassName.new^()` nodeCovering selects the parameter list but |
| // we want the constructor. |
| FormalParameterList(:var parent?) when offset == coveringNode.offset => |
| parent, |
| // In a constructor declaration with either a type name or a constructor |
| // name, we don't treat the keyword as something that has matches. It only |
| // has matches if it's `new()` or `factory()`. |
| ConstructorDeclaration( |
| :var typeName, |
| :var name, |
| newKeyword: var keyword?, |
| ) || |
| ConstructorDeclaration( |
| :var typeName, |
| :var name, |
| factoryKeyword: var keyword?, |
| ) |
| when (typeName != null || name != null) && |
| offset >= keyword.offset && |
| offset <= keyword.end => |
| null, |
| _ => coveringNode, |
| }; |
| } |
| |
| /// Computes the highlight target (elements and/or nodes) from the covering |
| /// node at the requested offset. |
| _HighlightTargets? _computeTargets(AstNode coveringNode) { |
| // Handle node targets (loop keyword etc.). |
| var targetNode = _getTargetNode(coveringNode); |
| if (targetNode != null) { |
| return _HighlightTargets.node(targetNode); |
| } |
| |
| // Add the obvious target element. |
| var mainTarget = _getTargetElement(coveringNode)?.canonical; |
| |
| // For pattern variables in implicit pattern fields (where the field name |
| // is inferred from the variable name), also include the field element. |
| |
| var additionalTarget = switch (coveringNode) { |
| VariablePattern(parent: PatternField(:var element, :var name)) |
| when name?.name == null => |
| element?.canonical, |
| _ => null, |
| }; |
| |
| // Include matching elements from superclasses as targets. |
| var allTargets = { |
| ?mainTarget, |
| ?additionalTarget, |
| ...?mainTarget?.supertypeMembers, |
| ...?additionalTarget?.supertypeMembers, |
| }; |
| |
| return _HighlightTargets.elements(mainTarget?.name, allTargets); |
| } |
| |
| /// Gets the target [AstNode] for [node] if it's a node-based highlight group |
| /// such as a loop keyword. |
| AstNode? _getTargetNode(AstNode node) { |
| return switch (node) { |
| // Loop/switch keywords are targets. |
| ForStatement() || |
| WhileStatement() || |
| DoStatement() || |
| SwitchStatement() => node, |
| |
| // Break/continue keywords target their respective targets. |
| BreakStatement(:var target?) || ContinueStatement(:var target?) => target, |
| |
| // Return/yield target the function body. |
| ReturnStatement() || |
| YieldStatement() => node.thisOrAncestorOfType<FunctionBody>(), |
| |
| _ => null, |
| }; |
| } |
| |
| /// Returns the target element for a given node, if one exists. |
| static Element? _getTargetElement(AstNode node) { |
| return switch (node) { |
| // We don't consider primary constructor bodies as something we ever |
| // provide highlights for. |
| PrimaryConstructorBody() => null, |
| |
| // In references to constructors where the constructor has no name, we map |
| // the (type) name to the constructor element. |
| NamedType(parent: ConstructorName(name: null, :var element?)) => element, |
| |
| // And in constructor declarations that do have names, we map the type |
| // name to the class element. |
| Identifier(parent: ConstructorDeclaration parent) |
| when parent.name != null && node == parent.typeName => |
| parent.declaredFragment?.element.enclosingElement, |
| |
| // For variable patterns with joins, use the base variable element. |
| DeclaredVariablePattern( |
| declaredFragment: BindPatternVariableFragment( |
| element: BindPatternVariableElement(:var join?), |
| ), |
| ) => |
| join, |
| |
| // Otherwise, default ElementLocator result. |
| _ => ElementLocator.locate(node), |
| }; |
| } |
| } |
| |
| class _DartDocumentHighlightsVisitor extends GeneralizingAstVisitor<void> { |
| final _HighlightTargets _target; |
| |
| /// Collected tokens matching the target. |
| final Set<Token> tokens = {}; |
| |
| /// Stack to track the current function for return/yield keywords. |
| final List<AstNode> _functionStack = []; |
| |
| _DartDocumentHighlightsVisitor(this._target); |
| |
| @override |
| void visitAssignedVariablePattern(AssignedVariablePattern node) { |
| var element = node.element; |
| if (element != null) { |
| _addOccurrence(element, node.name); |
| } |
| |
| super.visitAssignedVariablePattern(node); |
| } |
| |
| @override |
| void visitBreakStatement(BreakStatement node) { |
| _addNodeOccurrence(node.target, node.breakKeyword); |
| |
| super.visitBreakStatement(node); |
| } |
| |
| @override |
| void visitCatchClauseParameter(CatchClauseParameter node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitCatchClauseParameter(node); |
| } |
| |
| @override |
| void visitClassDeclaration(ClassDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.namePart.typeName); |
| |
| super.visitClassDeclaration(node); |
| } |
| |
| @override |
| void visitConstructorDeclaration(ConstructorDeclaration node) { |
| _addOccurrence( |
| node.declaredFragment?.element, |
| node.name ?? |
| node.typeName?.beginToken ?? |
| node.newKeyword ?? |
| node.factoryKeyword, |
| ); |
| |
| super.visitConstructorDeclaration(node); |
| } |
| |
| @override |
| void visitConstructorName(ConstructorName node) { |
| // For unnamed constructors, we add an occurence for the constructor at |
| // the location of the returnType. |
| if (node.name == null) { |
| var element = node.element; |
| if (element != null) { |
| _addOccurrence(element, node.type.name); |
| } |
| // Still visit the import prefix if there is one. |
| node.type.importPrefix?.accept(this); |
| return; // skip visitNamedType. |
| } |
| |
| super.visitConstructorName(node); |
| } |
| |
| @override |
| void visitContinueStatement(ContinueStatement node) { |
| _addNodeOccurrence(node.target, node.continueKeyword); |
| |
| super.visitContinueStatement(node); |
| } |
| |
| @override |
| void visitDeclaredIdentifier(DeclaredIdentifier node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitDeclaredIdentifier(node); |
| } |
| |
| @override |
| void visitDeclaredVariablePattern(DeclaredVariablePattern node) { |
| var declaredElement = node.declaredFragment?.element; |
| _addOccurrence(declaredElement?.join ?? declaredElement, node.name); |
| |
| super.visitDeclaredVariablePattern(node); |
| } |
| |
| @override |
| void visitDoStatement(DoStatement node) { |
| _addNodeOccurrence(node, node.doKeyword); |
| |
| super.visitDoStatement(node); |
| } |
| |
| @override |
| void visitEnumConstantDeclaration(EnumConstantDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitEnumConstantDeclaration(node); |
| } |
| |
| @override |
| void visitEnumDeclaration(EnumDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.namePart.typeName); |
| |
| super.visitEnumDeclaration(node); |
| } |
| |
| @override |
| void visitExtensionDeclaration(ExtensionDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitExtensionDeclaration(node); |
| } |
| |
| @override |
| void visitExtensionOverride(ExtensionOverride node) { |
| _addOccurrence(node.element, node.name); |
| |
| super.visitExtensionOverride(node); |
| } |
| |
| @override |
| void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) { |
| _addOccurrence( |
| node.declaredFragment?.element, |
| node.primaryConstructor.typeName, |
| ); |
| |
| super.visitExtensionTypeDeclaration(node); |
| } |
| |
| @override |
| void visitFormalParameter(FormalParameter node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitFormalParameter(node); |
| } |
| |
| @override |
| void visitForStatement(ForStatement node) { |
| _addNodeOccurrence(node, node.forKeyword); |
| |
| super.visitForStatement(node); |
| } |
| |
| @override |
| void visitFunctionBody(FunctionBody node) { |
| _functionStack.add(node); |
| super.visitFunctionBody(node); |
| _functionStack.removeLastOrNull(); |
| } |
| |
| @override |
| void visitFunctionDeclaration(FunctionDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitFunctionDeclaration(node); |
| } |
| |
| @override |
| void visitImportPrefixReference(ImportPrefixReference node) { |
| _addOccurrence(node.element, node.name); |
| |
| super.visitImportPrefixReference(node); |
| } |
| |
| @override |
| void visitMethodDeclaration(MethodDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitMethodDeclaration(node); |
| } |
| |
| @override |
| void visitMixinDeclaration(MixinDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitMixinDeclaration(node); |
| } |
| |
| @override |
| void visitNamedType(NamedType node) { |
| _addOccurrence(node.element, node.name); |
| |
| super.visitNamedType(node); |
| } |
| |
| @override |
| void visitPatternField(PatternField node) { |
| var pattern = node.pattern; |
| var name = node.name?.name; |
| |
| // If no explicit field name, use the variables name. |
| if (name == null && pattern is VariablePattern) { |
| name = pattern.name; |
| } |
| _addOccurrence(node.element, name); |
| |
| super.visitPatternField(node); |
| } |
| |
| @override |
| void visitPrimaryConstructorName(PrimaryConstructorName node) { |
| if (node.parent case PrimaryConstructorDeclaration primary) { |
| _addOccurrence(primary.declaredFragment?.element, node.name); |
| } |
| |
| super.visitPrimaryConstructorName(node); |
| } |
| |
| @override |
| void visitReturnStatement(ReturnStatement node) { |
| _addNodeOccurrence(_functionStack.lastOrNull, node.returnKeyword); |
| |
| super.visitReturnStatement(node); |
| } |
| |
| @override |
| void visitSimpleIdentifier(SimpleIdentifier node) { |
| // For unnamed constructors, we don't want to add an occurrence for the |
| // class name here because visitConstructorDeclaration will have added one |
| // for the constructor (not the type). |
| if (node.parent case ConstructorDeclaration( |
| :var name, |
| :var typeName, |
| ) when name == null && node == typeName) { |
| return; |
| } |
| |
| _addOccurrence(node.writeOrReadElement, node.token); |
| |
| return super.visitSimpleIdentifier(node); |
| } |
| |
| @override |
| void visitSwitchStatement(SwitchStatement node) { |
| _addNodeOccurrence(node, node.switchKeyword); |
| |
| super.visitSwitchStatement(node); |
| } |
| |
| @override |
| void visitTypeAlias(TypeAlias node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitTypeAlias(node); |
| } |
| |
| @override |
| void visitTypeParameter(TypeParameter node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitTypeParameter(node); |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration node) { |
| _addOccurrence(node.declaredFragment?.element, node.name); |
| |
| super.visitVariableDeclaration(node); |
| } |
| |
| @override |
| void visitWhileStatement(WhileStatement node) { |
| _addNodeOccurrence(node, node.whileKeyword); |
| |
| super.visitWhileStatement(node); |
| } |
| |
| @override |
| void visitYieldStatement(YieldStatement node) { |
| _addNodeOccurrence(_functionStack.lastOrNull, node.yieldKeyword); |
| |
| super.visitYieldStatement(node); |
| } |
| |
| void _addNodeOccurrence(AstNode? node, Token token) { |
| // Only add the occurrence if it matches our target node. |
| if (node != null && _target.matchesNode(node)) { |
| tokens.add(token); |
| } |
| } |
| |
| void _addOccurrence(Element? element, Token? token) { |
| if (element == null || token == null) return; |
| |
| var canonicalElement = element.canonical; |
| |
| if (canonicalElement == null) { |
| return; |
| } |
| |
| // Do a cheap name check before looking at the hierarchy. |
| if (!_target.matchesElementName(canonicalElement)) { |
| return; |
| } |
| |
| // This returns Iterable and will be lazily iterated by `any()` below if the |
| // canonical element doesn't match. |
| var supertypeMembers = canonicalElement.supertypeMembers; |
| |
| // Only add the occurrence if it's one of our target elements. |
| if (_target.matchesElement(canonicalElement) || |
| supertypeMembers.any(_target.matchesElement)) { |
| tokens.add(token); |
| } |
| } |
| } |
| |
| /// The highlight target(s) computed from the provided position. |
| /// |
| /// Usually this will contain a single element or a single node, however in some |
| /// cases (such as a variable pattern) there may be multiple target elements |
| /// (such as a variable and the matched getter). |
| class _HighlightTargets { |
| final String? _elementName; |
| final Set<Element> _targetElements; |
| final AstNode? _targetNode; |
| |
| _HighlightTargets.elements(this._elementName, this._targetElements) |
| : _targetNode = null; |
| |
| _HighlightTargets.node(this._targetNode) |
| : _elementName = null, |
| _targetElements = const {}; |
| |
| bool matchesElement(Element element) { |
| return _targetElements.contains(element); |
| } |
| |
| bool matchesElementName(Element canonicalElement) { |
| return canonicalElement.name == _elementName; |
| } |
| |
| bool matchesNode(AstNode node) { |
| return node == _targetNode; |
| } |
| } |
| |
| extension on Element { |
| /// Canonicalizes an element so that field formal parameters map to their |
| /// fields and property accessors map to their variables. |
| Element? get canonical { |
| return switch (this) { |
| FieldFormalParameterElement(:var field) => field?.baseElement, |
| PropertyAccessorElement(:var variable) |
| when variable.isOriginDeclaration => |
| variable.baseElement, |
| _ => baseElement, |
| }; |
| } |
| |
| /// All members in superclasses that this element overrides. |
| Iterable<Element> get supertypeMembers { |
| var enclosing = enclosingElement; |
| if (enclosing is! InterfaceElement) return const []; |
| |
| var name = Name.forElement(this); |
| if (name == null) return const []; |
| |
| var session = this.session; |
| if (session is! AnalysisSessionImpl) return const []; |
| |
| // Get this member for all supertypes. |
| var inheritanceManager = session.inheritanceManager; |
| return enclosing.allSupertypes |
| .map( |
| (supertype) => inheritanceManager.getMember(supertype.element, name), |
| ) |
| .map((element) => element?.canonical) |
| .nonNulls; |
| } |
| } |