Skip to content

Fix error spans for @satisfies#3212

Merged
RyanCavanaugh merged 7 commits into
microsoft:mainfrom
Andarist:fix/match-satisfies-error-span-highlight
May 29, 2026
Merged

Fix error spans for @satisfies#3212
RyanCavanaugh merged 7 commits into
microsoft:mainfrom
Andarist:fix/match-satisfies-error-span-highlight

Conversation

@Andarist

Copy link
Copy Markdown
Contributor

No description provided.

Comment thread internal/scanner/scanner.go Outdated
Comment on lines +2539 to +2550
for current := node.Parent; current != nil; current = current.Parent {
for _, jsDoc := range current.JSDoc(nil) {
if tags := jsDoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if ast.IsJSDocSatisfiesTag(tag) {
pos := SkipTrivia(sourceFile.Text(), tag.TagName().Pos())
return GetRangeOfTokenAtPosition(sourceFile, pos)
}
}
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super fond of this but I couldn't find a better way to go from a reparsed node to the original JSDoc right now, and I tried a bunch. Am I missing something?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure there's anything better, given we are in the scanner package and therefore if such a helper existed in astnav it would be unavailable.

But maybe @gabritto / @andrewbranch have a better thought on their minds.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a better way. However, I think you could pull out a helper and avoid the nil call, like:

func findOriginatingJSDocSatisfiesTag(sourceFile *ast.SourceFile, node *ast.Node) *ast.Node {
	targetType := node.AsSatisfiesExpression().Type
	if targetType.Flags&ast.NodeFlagsReparsed == 0 {
		return nil
	}
	for current := node.Parent; current != nil; current = current.Parent {
		if current.Flags&ast.NodeFlagsHasJSDoc == 0 {
			continue
		}
		var firstSatisfiesTag *ast.Node
		for _, jsDoc := range current.JSDoc(sourceFile) {
			if tags := jsDoc.AsJSDoc().Tags; tags != nil {
				for _, tag := range tags.Nodes {
					if !ast.IsJSDocSatisfiesTag(tag) {
						continue
					}
					if firstSatisfiesTag == nil {
						firstSatisfiesTag = tag
					}
					if typeExpr := tag.AsJSDocSatisfiesTag().TypeExpression; typeExpr != nil {
						if t := typeExpr.Type(); t != nil && t.Loc == targetType.Loc {
							return tag
						}
					}
				}
			}
		}
		return firstSatisfiesTag
	}
	return nil
}

Or something.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts diagnostic locations for JSDoc @satisfies so errors point at the satisfies tag (and related spans/columns) instead of the reparsed type node, aligning baseline expectations for conformance tests.

Changes:

  • Update GetErrorRangeForNode to map reparsed SatisfiesExpression errors back to the JSDoc @satisfies tag.
  • Change checkSatisfiesExpression to report assignability diagnostics on the SatisfiesExpression node (enabling the new error range behavior).
  • Refresh reference baseline outputs/diffs for several checkJsdocSatisfiesTag* conformance cases.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/scanner/scanner.go Special-cases error ranges for SatisfiesExpression with reparsed types to target the JSDoc @satisfies tag.
internal/checker/checker.go Reports satisfies assignability diagnostics on the expression node to use the updated error-span logic.
testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag1.errors.txt Updates expected error location/underline for @satisfies case.
testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag1.errors.txt.diff Updates divergence diff for the same case.
testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag4.errors.txt Updates expected error location/underline for @satisfies case.
testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag4.errors.txt.diff Updates divergence diff for the same case.
testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag12.errors.txt Updates expected error locations/underlines for multiple @satisfies scenarios.
testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag12.errors.txt.diff Updates divergence diff for the same cases.

Comment thread internal/scanner/scanner.go Outdated
Comment on lines +2539 to +2550
for current := node.Parent; current != nil; current = current.Parent {
for _, jsDoc := range current.JSDoc(nil) {
if tags := jsDoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if ast.IsJSDocSatisfiesTag(tag) {
pos := SkipTrivia(sourceFile.Text(), tag.TagName().Pos())
return GetRangeOfTokenAtPosition(sourceFile, pos)
}
}
}
}
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the satisfies type is reparsed, this returns the first @satisfies tag found on any ancestor JSDoc. If a JSDoc comment contains multiple @satisfies tags (which the reparser will apply by wrapping the initializer multiple times), this can highlight the wrong tag for inner/outer SatisfiesExpression nodes. Consider selecting the tag whose TypeExpression.Type().Loc (or pos/end) matches node.AsSatisfiesExpression().Type.Loc, and only fall back to the first tag if no match is found.

Suggested change
for current := node.Parent; current != nil; current = current.Parent {
for _, jsDoc := range current.JSDoc(nil) {
if tags := jsDoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if ast.IsJSDocSatisfiesTag(tag) {
pos := SkipTrivia(sourceFile.Text(), tag.TagName().Pos())
return GetRangeOfTokenAtPosition(sourceFile, pos)
}
}
}
}
}
targetType := node.AsSatisfiesExpression().Type
var firstSatisfiesTag *ast.Node
for current := node.Parent; current != nil; current = current.Parent {
for _, jsDoc := range current.JSDoc(nil) {
if tags := jsDoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if ast.IsJSDocSatisfiesTag(tag) {
if firstSatisfiesTag == nil {
firstSatisfiesTag = tag
}
typeExpr := tag.AsJSDocSatisfiesTag().TypeExpression()
if typeExpr != nil {
if typeNode := typeExpr.Type(); typeNode != nil {
if typeNode.Pos() == targetType.Pos() && typeNode.End() == targetType.End() {
pos := SkipTrivia(sourceFile.Text(), tag.TagName().Pos())
return GetRangeOfTokenAtPosition(sourceFile, pos)
}
}
}
}
}
}
}
}
if firstSatisfiesTag != nil {
pos := SkipTrivia(sourceFile.Text(), firstSatisfiesTag.TagName().Pos())
return GetRangeOfTokenAtPosition(sourceFile, pos)
}

Copilot uses AI. Check for mistakes.
Comment thread internal/scanner/scanner.go Outdated
Comment on lines +2539 to +2550
for current := node.Parent; current != nil; current = current.Parent {
for _, jsDoc := range current.JSDoc(nil) {
if tags := jsDoc.AsJSDoc().Tags; tags != nil {
for _, tag := range tags.Nodes {
if ast.IsJSDocSatisfiesTag(tag) {
pos := SkipTrivia(sourceFile.Text(), tag.TagName().Pos())
return GetRangeOfTokenAtPosition(sourceFile, pos)
}
}
}
}
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current.JSDoc(nil) triggers a GetSourceFileOfNode walk each time (since file is nil), and this code is already walking up the parent chain. This makes the reparsed-@satisfies path potentially O(depth^2). Consider computing sourceFile := ast.GetSourceFileOfNode(node) once and using current.JSDoc(sourceFile) inside the loop.

Copilot uses AI. Check for mistakes.
@RyanCavanaugh RyanCavanaugh modified the milestone: TypeScript 7.0 RC Apr 17, 2026
@Andarist Andarist requested a review from jakebailey May 18, 2026 09:40
@jakebailey

Copy link
Copy Markdown
Member

This is failing all the tests

Comment thread internal/scanner/scanner.go Outdated
continue
}
var firstSatisfiesTag *ast.Node
for _, jsDoc := range current.JSDoc(sourceFile) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be EagerJSDoc to avoid parsing JSDoc in TS files with satisifes, or is that not what happens?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already kinda guarded by NodeFlagsReparsed - so it shouldn't end up parsing JSDoc in TS files

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, though we continue, so we are still going to walk up the entire tree to the SourceFile?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only walked when there is an error to be raised - so it should hit some actual node with @satisfies before reaching the SourceFile

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, though I think I'd still like to see EagerJSDoc used just because the checker does not actually call JSDoc at all when working with these tags, I think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed out this change, so you'll have to approve the PR again

jakebailey
jakebailey previously approved these changes May 19, 2026
@jakebailey jakebailey dismissed their stale review May 19, 2026 15:01

Forgot my comment

@Andarist

Copy link
Copy Markdown
Contributor Author

This is failing all the tests

sorry about that, the failures crept in here after syncing with main ;p

@gabritto gabritto added this pull request to the merge queue May 29, 2026
@gabritto gabritto removed this pull request from the merge queue due to a manual request May 29, 2026
@RyanCavanaugh RyanCavanaugh added this pull request to the merge queue May 29, 2026
Merged via the queue into microsoft:main with commit 9a58775 May 29, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants