Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions internal/fourslash/tests/statedeclarationmaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,47 @@ fn5();
})
}
}

// TestDeclarationMapsNonMonotonicMappings verifies that getMappedLocation clamps
// inverted ranges caused by non-monotonic source map mappings.
//
// The baseline comparison catches regressions: without the fix, the output shows
// inverted markers (e.g., "|]|>" appearing before "<|"), while with the fix,
// the ranges are clamped to valid zero-length ranges (e.g., "<||>").
func TestDeclarationMapsNonMonotonicMappings(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
// The source map creates a non-monotonic mapping:
// - .d.ts line 0 col 24 ('b' identifier) -> source line 1, col 16
// - .d.ts line 0 col 25 (right after 'b') -> source line 0, col 0 (EARLIER!)
//
// When looking up 'b' identifier [24, 25), start maps to ~byte 39,
// but end maps to byte 0, creating an inverted range.
// The fix in getMappedLocation clamps this to prevent negative ranges.
const content = `
// @Filename: /src/index.ts
export function a() {}
export function b() {}
// @Filename: /src/indexdef.d.ts.map
{
"version": 3,
"file": "indexdef.d.ts",
"sourceRoot": "",
"sources": ["index.ts"],
"names": [],
"mappings": "AACA,wBAAgB,CADhB;AAAA,wBAAgB"
}
// @Filename: /src/indexdef.d.ts
export declare function b(): void;
export declare function a(): void;
//# sourceMappingURL=indexdef.d.ts.map
// @Filename: /src/user.ts
import { a, b } from "./indexdef";
/*1*/a();
/*2*/b();
// @Filename: /src/tsconfig.json
{}`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyBaselineGoToDefinition(t, true, "1", "2")
}
12 changes: 11 additions & 1 deletion internal/ls/source_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ func (l *LanguageService) getMappedLocation(fileName string, fileRange core.Text
}
}
debug.Assert(endPos.FileName == startPos.FileName, "start and end should be in same file")
newRange := core.NewTextRange(startPos.Pos, endPos.Pos)

// Validate that the end position is valid (same file and after start position).
// This handles non-monotonic source map mappings where the next mapping entry
// might have an original position that's earlier in the source file.
// If end is before start, clamp to start position to avoid inverted ranges.
endPosValue := endPos.Pos
if endPos.FileName != startPos.FileName || endPos.Pos < startPos.Pos {
endPosValue = startPos.Pos
}

newRange := core.NewTextRange(startPos.Pos, endPosValue)
lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName))
return lsproto.Location{
Uri: lsconv.FileNameToDocumentURI(startPos.FileName),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// === goToDefinition ===
// === /src/index.ts ===
// <|export function [|a|]() {}
// export func|>tion b() {}

// === /src/user.ts ===
// import { a, b } from "./indexdef";
// /*GOTO DEF*/[|a|]();
// b();



// === goToDefinition ===
// === /src/index.ts ===
// export function a() {}
// <||>export function [|{ contextId: 0 |}|]b() {}

// === /src/user.ts ===
// import { a, b } from "./indexdef";
// a();
// /*GOTO DEF*/[|b|]();
Loading