Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7a4b3e2
merged server from main
johnfav03 Aug 28, 2025
0b437c2
implemented highlights and helper functions
johnfav03 Aug 28, 2025
57fa39d
Merge remote-tracking branch 'origin/main' into document-highlights
johnfav03 Sep 9, 2025
7342e59
fourslash testing for doc highlights
johnfav03 Sep 9, 2025
45906a2
fix compare LSP positions
gabritto Sep 10, 2025
a20e054
fix tests
gabritto Sep 10, 2025
c076d4b
update baselines
gabritto Sep 10, 2025
b08db3a
Merge remote-tracking branch 'origin/main' into document-highlights
johnfav03 Sep 10, 2025
c0fac1d
Merge branch 'gabritto/fixcompare' into document-highlights
johnfav03 Sep 10, 2025
a76671b
merged main
johnfav03 Sep 16, 2025
2fd1904
Merge remote-tracking branch 'origin/main' into document-highlights
johnfav03 Sep 17, 2025
6d814bb
documentHighlights with fourslash tests
johnfav03 Sep 17, 2025
1dade87
fixed minor change
johnfav03 Sep 17, 2025
e9d2bd6
removed unused baselines
johnfav03 Sep 17, 2025
45cd07b
removed repeated test
johnfav03 Sep 18, 2025
48e3163
fixed var declaration and nil checking nits
johnfav03 Sep 18, 2025
9c1245e
Merge branch 'main' into document-highlights
johnfav03 Sep 18, 2025
318a6f7
moved isWriteAccess and helpers from checker to ast
johnfav03 Sep 18, 2025
2d661ce
refactored renameArg and docHighlightArg into markerOrRangeArg
johnfav03 Sep 22, 2025
508be4d
fixed jsx case handling
johnfav03 Sep 22, 2025
a13337c
Merge remote-tracking branch 'origin' into document-highlights
johnfav03 Sep 24, 2025
9b512a5
added isWriteAccessForReference logic
johnfav03 Sep 24, 2025
ea32d43
cleaned up comments
johnfav03 Sep 24, 2025
d446a93
documenthighlights.go code and comment cleanup
johnfav03 Sep 24, 2025
1dcecaf
Update internal/fourslash/_scripts/convertFourslash.mts
johnfav03 Sep 24, 2025
0ec6d31
removed extraneous newline
johnfav03 Sep 24, 2025
2dd5588
minor test fix
johnfav03 Sep 25, 2025
f8c43e1
fixed out of bounds in findReferencedSymbolsForNode
johnfav03 Sep 25, 2025
e903fae
reupdated failing tests
johnfav03 Sep 25, 2025
016ab91
fixed ci check for convertfourslash
johnfav03 Sep 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -2188,6 +2188,244 @@ type (
JsxAttributeList = NodeList // NodeList[*JsxAttributeLike]
)

func IsWriteOnlyAccess(node *Node) bool {
return accessKind(node) == AccessKindWrite
}

func IsWriteAccess(node *Node) bool {
return accessKind(node) != AccessKindRead
}

func IsWriteAccessForReference(node *Node) bool {
decl := getDeclarationFromName(node)
return (decl != nil && declarationIsWriteAccess(decl)) || node.Kind == KindDefaultKeyword || IsWriteAccess(node)
}

func getDeclarationFromName(name *Node) *Declaration {
if name == nil || name.Parent == nil {
return nil
}
parent := name.Parent
switch name.Kind {
case KindStringLiteral, KindNoSubstitutionTemplateLiteral, KindNumericLiteral:
if IsComputedPropertyName(parent) {
return parent.Parent
}
fallthrough
case KindIdentifier:
if IsDeclaration(parent) {
if parent.Name() == name {
return parent
}
return nil
}
if IsQualifiedName(parent) {
tag := parent.Parent
if IsJSDocParameterTag(tag) && tag.Name() == parent {
return tag
}
return nil
}
binExp := parent.Parent
if IsBinaryExpression(binExp) && GetAssignmentDeclarationKind(binExp.AsBinaryExpression()) != JSDeclarationKindNone {
// (binExp.left as BindableStaticNameExpression).symbol || binExp.symbol
leftHasSymbol := false
if binExp.AsBinaryExpression().Left != nil && binExp.AsBinaryExpression().Left.Symbol() != nil {
leftHasSymbol = true
}
if leftHasSymbol || binExp.Symbol() != nil {
if GetNameOfDeclaration(binExp.AsNode()) == name {
return binExp.AsNode()
}
}
}
case KindPrivateIdentifier:
if IsDeclaration(parent) && parent.Name() == name {
return parent
}
}
return nil
}

func declarationIsWriteAccess(decl *Node) bool {
if decl == nil {
return false
}
// Consider anything in an ambient declaration to be a write access since it may be coming from JS.
if decl.Flags&NodeFlagsAmbient != 0 {
return true
}

switch decl.Kind {
case KindBinaryExpression,
KindBindingElement,
KindClassDeclaration,
KindClassExpression,
KindDefaultKeyword,
KindEnumDeclaration,
KindEnumMember,
KindExportSpecifier,
KindImportClause, // default import
KindImportEqualsDeclaration,
KindImportSpecifier,
KindInterfaceDeclaration,
KindJSDocCallbackTag,
KindJSDocTypedefTag,
KindJsxAttribute,
KindModuleDeclaration,
KindNamespaceExportDeclaration,
KindNamespaceImport,
KindNamespaceExport,
KindParameter,
KindShorthandPropertyAssignment,
KindTypeAliasDeclaration,
KindTypeParameter:
return true

case KindPropertyAssignment:
// In `({ x: y } = 0);`, `x` is not a write access.
return !IsArrayLiteralOrObjectLiteralDestructuringPattern(decl.Parent)

case KindFunctionDeclaration, KindFunctionExpression, KindConstructor, KindMethodDeclaration, KindGetAccessor, KindSetAccessor:
// functions considered write if they provide a value (have a body)
switch decl.Kind {
case KindFunctionDeclaration:
return decl.AsFunctionDeclaration().Body != nil
case KindFunctionExpression:
return decl.AsFunctionExpression().Body != nil
case KindConstructor:
// constructor node stores body on the parent? treat same as others
return decl.AsConstructorDeclaration().Body != nil
case KindMethodDeclaration:
return decl.AsMethodDeclaration().Body != nil
case KindGetAccessor:
return decl.AsGetAccessorDeclaration().Body != nil
case KindSetAccessor:
return decl.AsSetAccessorDeclaration().Body != nil
}
return false

case KindVariableDeclaration, KindPropertyDeclaration:
// variable/property write if initializer present or is in catch clause
var hasInit bool
switch decl.Kind {
case KindVariableDeclaration:
hasInit = decl.AsVariableDeclaration().Initializer != nil
case KindPropertyDeclaration:
hasInit = decl.AsPropertyDeclaration().Initializer != nil
}
return hasInit || IsCatchClause(decl.Parent)

case KindMethodSignature, KindPropertySignature, KindJSDocPropertyTag, KindJSDocParameterTag:
return false

default:
// preserve TS behavior: crash on unexpected kinds
panic("Unhandled case in declarationIsWriteAccess")
}
}

func IsArrayLiteralOrObjectLiteralDestructuringPattern(node *Node) bool {
if !(IsArrayLiteralExpression(node) || IsObjectLiteralExpression(node)) {
return false
}
parent := node.Parent
// [a,b,c] from:
// [a, b, c] = someExpression;
if IsBinaryExpression(parent) && parent.AsBinaryExpression().Left == node && parent.AsBinaryExpression().OperatorToken.Kind == KindEqualsToken {
return true
}
// [a, b, c] from:
// for([a, b, c] of expression)
if IsForOfStatement(parent) && parent.Initializer() == node {
return true
}
// {x, a: {a, b, c} } = someExpression
if IsPropertyAssignment(parent) {
return IsArrayLiteralOrObjectLiteralDestructuringPattern(parent.Parent)
}
// [a, b, c] of
// [x, [a, b, c] ] = someExpression
return IsArrayLiteralOrObjectLiteralDestructuringPattern(parent)
}

func accessKind(node *Node) AccessKind {
parent := node.Parent
switch parent.Kind {
case KindParenthesizedExpression:
return accessKind(parent)
case KindPrefixUnaryExpression:
operator := parent.AsPrefixUnaryExpression().Operator
if operator == KindPlusPlusToken || operator == KindMinusMinusToken {
return AccessKindReadWrite
}
return AccessKindRead
case KindPostfixUnaryExpression:
operator := parent.AsPostfixUnaryExpression().Operator
if operator == KindPlusPlusToken || operator == KindMinusMinusToken {
return AccessKindReadWrite
}
return AccessKindRead
case KindBinaryExpression:
if parent.AsBinaryExpression().Left == node {
operator := parent.AsBinaryExpression().OperatorToken
if IsAssignmentOperator(operator.Kind) {
if operator.Kind == KindEqualsToken {
return AccessKindWrite
}
return AccessKindReadWrite
}
}
return AccessKindRead
case KindPropertyAccessExpression:
if parent.AsPropertyAccessExpression().Name() != node {
return AccessKindRead
}
return accessKind(parent)
case KindPropertyAssignment:
parentAccess := accessKind(parent.Parent)
// In `({ x: varname }) = { x: 1 }`, the left `x` is a read, the right `x` is a write.
if node == parent.AsPropertyAssignment().Name() {
return reverseAccessKind(parentAccess)
}
return parentAccess
case KindShorthandPropertyAssignment:
// Assume it's the local variable being accessed, since we don't check public properties for --noUnusedLocals.
if node == parent.AsShorthandPropertyAssignment().ObjectAssignmentInitializer {
return AccessKindRead
}
return accessKind(parent.Parent)
case KindArrayLiteralExpression:
return accessKind(parent)
case KindForInStatement, KindForOfStatement:
if node == parent.AsForInOrOfStatement().Initializer {
return AccessKindWrite
}
return AccessKindRead
}
return AccessKindRead
}

func reverseAccessKind(a AccessKind) AccessKind {
switch a {
case AccessKindRead:
return AccessKindWrite
case AccessKindWrite:
return AccessKindRead
case AccessKindReadWrite:
return AccessKindReadWrite
}
panic("Unhandled case in reverseAccessKind")
}

type AccessKind int32

const (
AccessKindRead AccessKind = iota // Only reads from a variable
AccessKindWrite // Only writes to a variable without ever reading it. E.g.: `x=1;`.
AccessKindReadWrite // Reads from and writes to a variable. E.g.: `f(x++);`, `x/=1`.
)

// DeclarationBase

type DeclarationBase struct {
Expand Down Expand Up @@ -3191,6 +3429,10 @@ func (node *ThrowStatement) computeSubtreeFacts() SubtreeFacts {
return propagateSubtreeFacts(node.Expression)
}

func IsThrowStatement(node *Node) bool {
return node.Kind == KindThrowStatement
}

// TryStatement

type TryStatement struct {
Expand Down Expand Up @@ -6153,6 +6395,10 @@ func (node *YieldExpression) computeSubtreeFacts() SubtreeFacts {
return propagateSubtreeFacts(node.Expression) | SubtreeContainsForAwaitOrAsyncGenerator
}

func IsYieldExpression(node *Node) bool {
return node.Kind == KindYieldExpression
}

// ArrowFunction

type ArrowFunction struct {
Expand Down
14 changes: 7 additions & 7 deletions internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10873,15 +10873,15 @@ func (c *Checker) checkPropertyAccessExpressionOrQualifiedName(node *ast.Node, l
c.checkPropertyNotUsedBeforeDeclaration(prop, node, right)
c.markPropertyAsReferenced(prop, node, c.isSelfTypeAccess(left, parentSymbol))
c.symbolNodeLinks.Get(node).resolvedSymbol = prop
c.checkPropertyAccessibility(node, left.Kind == ast.KindSuperKeyword, isWriteAccess(node), apparentType, prop)
c.checkPropertyAccessibility(node, left.Kind == ast.KindSuperKeyword, ast.IsWriteAccess(node), apparentType, prop)
if c.isAssignmentToReadonlyEntity(node, prop, assignmentKind) {
c.error(right, diagnostics.Cannot_assign_to_0_because_it_is_a_read_only_property, right.Text())
return c.errorType
}
switch {
case c.isThisPropertyAccessInConstructor(node, prop):
propType = c.autoType
case writeOnly || isWriteOnlyAccess(node):
case writeOnly || ast.IsWriteOnlyAccess(node):
propType = c.getWriteTypeOfSymbol(prop)
default:
propType = c.getTypeOfSymbol(prop)
Expand Down Expand Up @@ -13284,7 +13284,7 @@ func (c *Checker) getResolvedSymbol(node *ast.Node) *ast.Symbol {
var symbol *ast.Symbol
if !ast.NodeIsMissing(node) {
symbol = c.resolveName(node, node.AsIdentifier().Text, ast.SymbolFlagsValue|ast.SymbolFlagsExportValue,
c.getCannotFindNameDiagnosticForName(node), !isWriteOnlyAccess(node), false /*excludeGlobals*/)
c.getCannotFindNameDiagnosticForName(node), !ast.IsWriteOnlyAccess(node), false /*excludeGlobals*/)
}
links.resolvedSymbol = core.OrElse(symbol, c.unknownSymbol)
}
Expand Down Expand Up @@ -15719,9 +15719,9 @@ func (c *Checker) GetTypeOfSymbolAtLocation(symbol *ast.Symbol, location *ast.No
if ast.IsRightSideOfQualifiedNameOrPropertyAccess(location) {
location = location.Parent
}
if ast.IsExpressionNode(location) && (!ast.IsAssignmentTarget(location) || isWriteAccess(location)) {
if ast.IsExpressionNode(location) && (!ast.IsAssignmentTarget(location) || ast.IsWriteAccess(location)) {
var t *Type
if isWriteAccess(location) && location.Kind == ast.KindPropertyAccessExpression {
if ast.IsWriteAccess(location) && location.Kind == ast.KindPropertyAccessExpression {
t = c.checkPropertyAccessExpression(location, CheckModeNormal, true /*writeOnly*/)
} else {
t = c.getTypeOfExpression(location)
Expand All @@ -15739,7 +15739,7 @@ func (c *Checker) GetTypeOfSymbolAtLocation(symbol *ast.Symbol, location *ast.No
// to it at the given location. Since we have no control flow information for the
// hypothetical reference (control flow information is created and attached by the
// binder), we simply return the declared type of the symbol.
if isRightSideOfAccessExpression(location) && isWriteAccess(location.Parent) {
if isRightSideOfAccessExpression(location) && ast.IsWriteAccess(location.Parent) {
return c.getWriteTypeOfSymbol(symbol)
}
}
Expand Down Expand Up @@ -26711,7 +26711,7 @@ func (c *Checker) markPropertyAsReferenced(prop *ast.Symbol, nodeForCheckWriteOn
if !hasPrivateModifier && !hasPrivateIdentifier {
return
}
if nodeForCheckWriteOnly != nil && isWriteOnlyAccess(nodeForCheckWriteOnly) && prop.Flags&ast.SymbolFlagsSetAccessor == 0 {
if nodeForCheckWriteOnly != nil && ast.IsWriteOnlyAccess(nodeForCheckWriteOnly) && prop.Flags&ast.SymbolFlagsSetAccessor == 0 {
return
}
if isSelfTypeAccess {
Expand Down
26 changes: 1 addition & 25 deletions internal/checker/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,7 @@ func (c *Checker) GetPropertySymbolsFromContextualType(node *ast.Node, contextua
//
// [a] = [ property1, property2 ]
func (c *Checker) GetPropertySymbolOfDestructuringAssignment(location *ast.Node) *ast.Symbol {
if isArrayLiteralOrObjectLiteralDestructuringPattern(location.Parent.Parent) {
if ast.IsArrayLiteralOrObjectLiteralDestructuringPattern(location.Parent.Parent) {
// Get the type of the object or array literal and then look for property of given name in the type
if typeOfObjectLiteral := c.getTypeOfAssignmentPattern(location.Parent.Parent); typeOfObjectLiteral != nil {
return c.getPropertyOfType(typeOfObjectLiteral, location.Text())
Expand Down Expand Up @@ -809,27 +809,3 @@ func (c *Checker) getTypeOfAssignmentPattern(expr *ast.Node) *Type {
elementType := core.OrElse(c.checkIteratedTypeOrElementType(IterationUseDestructuring, typeOfArrayLiteral, c.undefinedType, expr.Parent), c.errorType)
return c.checkArrayLiteralDestructuringElementAssignment(node, typeOfArrayLiteral, slices.Index(node.AsArrayLiteralExpression().Elements.Nodes, expr), elementType, CheckModeNormal)
}

func isArrayLiteralOrObjectLiteralDestructuringPattern(node *ast.Node) bool {
if !(ast.IsArrayLiteralExpression(node) || ast.IsObjectLiteralExpression(node)) {
return false
}
parent := node.Parent
// [a,b,c] from:
// [a, b, c] = someExpression;
if ast.IsBinaryExpression(parent) && parent.AsBinaryExpression().Left == node && parent.AsBinaryExpression().OperatorToken.Kind == ast.KindEqualsToken {
return true
}
// [a, b, c] from:
// for([a, b, c] of expression)
if ast.IsForOfStatement(parent) && parent.Initializer() == node {
return true
}
// {x, a: {a, b, c} } = someExpression
if ast.IsPropertyAssignment(parent) {
return isArrayLiteralOrObjectLiteralDestructuringPattern(parent.Parent)
}
// [a, b, c] of
// [x, [a, b, c] ] = someExpression
return isArrayLiteralOrObjectLiteralDestructuringPattern(parent)
}
Loading
Loading