Skip to content
Merged
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
2 changes: 1 addition & 1 deletion microsoft/typescript-go
Submodule typescript-go updated 39 files
+6 −6 .github/actions/setup-go/action.yml
+3 −7 .github/workflows/ci.yml
+3 −3 .github/workflows/codeql.yml
+9 −0 _extension/package.json
+15 −0 _extension/src/client.ts
+3 −2 internal/ast/ast.go
+2 −18 internal/astnav/tokens.go
+15 −25 internal/checker/checker.go
+114 −20 internal/fourslash/_scripts/updateFailing.mts
+1 −4 internal/ls/folding.go
+17 −1 internal/ls/hover.go
+1 −1 internal/parser/parser.go
+7 −7 internal/parser/reparser.go
+1 −0 internal/testutil/tsbaseline/type_symbol_baseline.go
+2 −5 internal/transformers/tstransforms/legacydecorators.go
+17 −0 testdata/baselines/reference/compiler/checkInheritedProperty.errors.txt
+26 −0 testdata/baselines/reference/compiler/checkInheritedProperty.symbols
+26 −0 testdata/baselines/reference/compiler/checkInheritedProperty.types
+23 −0 testdata/baselines/reference/compiler/legacyDecoratorClassWithoutModifiers.js
+15 −0 testdata/baselines/reference/compiler/legacyDecoratorClassWithoutModifiers.symbols
+15 −0 testdata/baselines/reference/compiler/legacyDecoratorClassWithoutModifiers.types
+37 −0 testdata/baselines/reference/compiler/legacyDecoratorsSingleAccessor.js
+30 −0 testdata/baselines/reference/compiler/legacyDecoratorsSingleAccessor.symbols
+29 −0 testdata/baselines/reference/compiler/legacyDecoratorsSingleAccessor.types
+2 −2 testdata/baselines/reference/submodule/compiler/sourceMapValidationDecorators.js.map
+2 −2 testdata/baselines/reference/submodule/compiler/sourceMapValidationDecorators.js.map.diff
+10 −10 testdata/baselines/reference/submodule/compiler/sourceMapValidationDecorators.sourcemap.txt
+10 −34 testdata/baselines/reference/submodule/compiler/sourceMapValidationDecorators.sourcemap.txt.diff
+2 −2 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag1.errors.txt
+2 −2 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag1.errors.txt.diff
+0 −1 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag11.types
+0 −9 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag11.types.diff
+6 −6 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag12.errors.txt
+10 −10 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag12.errors.txt.diff
+2 −2 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag4.errors.txt
+2 −2 testdata/baselines/reference/submodule/conformance/checkJsdocSatisfiesTag4.errors.txt.diff
+12 −0 testdata/tests/cases/compiler/checkInheritedProperty.ts
+9 −0 testdata/tests/cases/compiler/legacyDecoratorClassWithoutModifiers.ts
+14 −0 testdata/tests/cases/compiler/legacyDecoratorsSingleAccessor.ts
5 changes: 3 additions & 2 deletions pkg/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -11001,7 +11001,6 @@ func (node *SourceFile) GetOrCreateToken(
) *TokenNode {
node.tokenCacheMu.Lock()
defer node.tokenCacheMu.Unlock()

loc := core.NewTextRange(pos, end)
if node.tokenCache == nil {
node.tokenCache = make(map[core.TextRange]*Node)
Expand All @@ -11014,7 +11013,9 @@ func (node *SourceFile) GetOrCreateToken(
}
return token
}

if parent.Flags&NodeFlagsReparsed != 0 {
panic(fmt.Sprintf("Cannot create token from reparsed node of kind %v", parent.Kind))
}
token := createToken(kind, node, pos, end, flags)
token.Loc = loc
token.Parent = parent
Expand Down
20 changes: 2 additions & 18 deletions pkg/astnav/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ func getTokenAtPosition(
// `left` tracks the lower boundary of the node/token that could be returned,
// and is eventually the scanner's start position, if the scanner is used.
left := 0
// `allowReparsed` is set when we're navigating inside an AsExpression or
// SatisfiesExpression, which allows visiting their reparsed children to reach
// the actual identifier from JSDoc type assertions.
allowReparsed := false

testNode := func(node *ast.Node) int {
if node.Kind != ast.KindEndOfFile && node.End() == position && includePrecedingTokenAtEndPosition != nil {
Expand All @@ -92,15 +88,8 @@ func getTokenAtPosition(
// We can't abort visiting children, so once a match is found, we set `next`
// and do nothing on subsequent visits.
if node != nil && next == nil {
// Skip reparsed nodes unless:
// 1. The node itself is AsExpression or SatisfiesExpression, OR
// 2. We're already inside an AsExpression or SatisfiesExpression (allowReparsed=true)
// These are special cases where reparsed nodes from JSDoc type assertions
// should still be navigable to reach identifiers.
isSpecialReparsed := node.Flags&ast.NodeFlagsReparsed != 0 &&
(node.Kind == ast.KindAsExpression || node.Kind == ast.KindSatisfiesExpression)

if node.Flags&ast.NodeFlagsReparsed == 0 || isSpecialReparsed || allowReparsed {
// Skip reparsed nodes
if node.Flags&ast.NodeFlagsReparsed == 0 {
result := testNode(node)
switch result {
case -1:
Expand Down Expand Up @@ -211,11 +200,6 @@ func getTokenAtPosition(
current = next
left = current.Pos()
next = nil
// When navigating into AsExpression or SatisfiesExpression, allow visiting
// their reparsed children to reach identifiers from JSDoc type assertions.
if current.Kind == ast.KindAsExpression || current.Kind == ast.KindSatisfiesExpression {
allowReparsed = true
}
}
}

Expand Down
40 changes: 15 additions & 25 deletions pkg/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -10408,18 +10408,15 @@ func (c *Checker) getInstantiationExpressionType(exprType *Type, node *ast.Node)
}

func (c *Checker) checkSatisfiesExpression(node *ast.Node) *Type {
c.checkSourceElement(node.Type())
return c.checkSatisfiesExpressionWorker(node.Expression(), node.Type(), CheckModeNormal)
}

func (c *Checker) checkSatisfiesExpressionWorker(expression *ast.Node, target *ast.Node, checkMode CheckMode) *Type {
exprType := c.checkExpressionEx(expression, checkMode)
targetType := c.getTypeFromTypeNode(target)
typeNode := node.Type()
c.checkSourceElement(typeNode)
exprType := c.checkExpression(node.Expression())
targetType := c.getTypeFromTypeNode(typeNode)
if c.isErrorType(targetType) {
return targetType
}
errorNode := ast.FindAncestor(target.Parent, func(n *ast.Node) bool { return ast.IsSatisfiesExpression(n) })
c.checkTypeAssignableToAndOptionallyElaborate(exprType, targetType, errorNode, expression, diagnostics.Type_0_does_not_satisfy_the_expected_type_1, nil)
errorNode := core.IfElse(typeNode.Flags&ast.NodeFlagsReparsed != 0, typeNode, node)
c.checkTypeAssignableToAndOptionallyElaborate(exprType, targetType, errorNode, node.Expression(), diagnostics.Type_0_does_not_satisfy_the_expected_type_1, nil)
return exprType
}

Expand Down Expand Up @@ -11371,21 +11368,13 @@ func (c *Checker) isOptionalPropertyDeclaration(node *ast.Node) bool {
}

func (c *Checker) isPropertyDeclaredInAncestorClass(prop *ast.Symbol) bool {
if prop.Parent.Flags&ast.SymbolFlagsClass == 0 {
return false
}
classType := c.getDeclaredTypeOfSymbol(prop.Parent)
for {
baseTypes := c.getBaseTypes(classType)
if len(baseTypes) == 0 {
return false
}
classType = baseTypes[0]
superProperty := c.getPropertyOfType(classType, prop.Name)
if superProperty != nil && superProperty.ValueDeclaration != nil {
return true
if prop.Parent.Flags&ast.SymbolFlagsClass != 0 {
if baseTypes := c.getBaseTypes(c.getDeclaredTypeOfSymbol(prop.Parent)); len(baseTypes) != 0 {
superProperty := c.getPropertyOfType(baseTypes[0], prop.Name)
return superProperty != nil && superProperty.ValueDeclaration != nil
}
}
return false
}

/**
Expand Down Expand Up @@ -11935,14 +11924,15 @@ func (c *Checker) checkAssertion(node *ast.Node, checkMode CheckMode) *Type {
}

func (c *Checker) checkAssertionDeferred(node *ast.Node) {
typeNode := node.Type()
exprType := c.getRegularTypeOfObjectLiteral(c.getBaseTypeOfLiteralType(c.assertionLinks.Get(node).exprType))
targetType := c.getTypeFromTypeNode(node.Type())
targetType := c.getTypeFromTypeNode(typeNode)
if !c.isErrorType(targetType) {
widenedType := c.getWidenedType(exprType)
if !c.isTypeComparableTo(targetType, widenedType) {
errNode := node
if node.Flags&ast.NodeFlagsReparsed != 0 {
errNode = node.Type()
if typeNode.Flags&ast.NodeFlagsReparsed != 0 {
errNode = typeNode
}
c.checkTypeComparableTo(exprType, targetType, errNode, diagnostics.Conversion_of_type_0_to_type_1_may_be_a_mistake_because_neither_type_sufficiently_overlaps_with_the_other_If_this_was_intentional_convert_the_expression_to_unknown_first)
}
Expand Down
134 changes: 114 additions & 20 deletions pkg/fourslash/_scripts/updateFailing.mts
Original file line number Diff line number Diff line change
@@ -1,43 +1,137 @@
import * as cp from "child_process";
import * as fs from "fs";
import path from "path";
import * as readline from "readline";
import which from "which";

const failingTestsPath = path.join(import.meta.dirname, "failingTests.txt");
const crashingTestsPath = path.join(import.meta.dirname, "crashingTests.txt");

function main() {
interface TestEvent {
Time?: string;
Action: string;
Package?: string;
Test?: string;
Output?: string;
Elapsed?: number;
}

async function main() {
const go = which.sync("go");
let testOutput: string;

let testProcess: cp.ChildProcess;
try {
// Run tests with TSGO_FOURSLASH_IGNORE_FAILING=1 to run all tests including those in failingTests.txt
testOutput = cp.execFileSync(go, ["test", "-v", "./internal/fourslash/tests/gen"], {
encoding: "utf-8",
testProcess = cp.spawn(go, ["test", "-json", "./internal/fourslash/tests/gen"], {
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env, TSGO_FOURSLASH_IGNORE_FAILING: "1" },
});
}
catch (error) {
testOutput = (error as { stdout: string; }).stdout as string;
throw new Error("Failed to spawn test process: " + error);
}
const panicRegex = /^panic/m;
if (panicRegex.test(testOutput)) {
throw new Error("Unrecovered panic detected in tests\n" + testOutput);

if (!testProcess.stdout || !testProcess.stderr) {
throw new Error("Test process stdout or stderr is null");
}
const failRegex = /--- FAIL: ([\S]+)/gm;

const failingTests: string[] = [];
const crashingRegex = /^=== (?:NAME|CONT) ([\S]+)\n.*InternalError.*$/gm;
const crashingTests: string[] = [];
let match;
const testOutputs = new Map<string, string[]>();
const allOutputs: string[] = [];
let hadPanic = false;

while ((match = failRegex.exec(testOutput)) !== null) {
failingTests.push(match[1]);
}
while ((match = crashingRegex.exec(testOutput)) !== null) {
crashingTests.push(match[1]);
}
const rl = readline.createInterface({
input: testProcess.stdout,
crlfDelay: Infinity,
});

rl.on("line", line => {
try {
const event: TestEvent = JSON.parse(line);

// Collect output for each test
if (event.Action === "output" && event.Output) {
allOutputs.push(event.Output);
if (event.Test) {
if (!testOutputs.has(event.Test)) {
testOutputs.set(event.Test, []);
}
testOutputs.get(event.Test)!.push(event.Output);
}

// Check for panics
if (/^panic/m.test(event.Output)) {
hadPanic = true;
}
}

// Process failed tests
if (event.Action === "fail" && event.Test) {
const outputs = testOutputs.get(event.Test) || [];

fs.writeFileSync(failingTestsPath, failingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
fs.writeFileSync(crashingTestsPath, crashingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
// Check if this is a crashing test (contains InternalError)
const hasCrash = outputs.some(line => line.includes("InternalError"));
if (hasCrash) {
crashingTests.push(event.Test);
}

// A test is only considered a baseline-only failure if ALL error messages
// are baseline-related. Any non-baseline error message means it's a real failure.
const baselineMessagePatterns = [
/^\s*baseline\.go:\d+: the baseline file .* has changed\./,
/^\s*baseline\.go:\d+: new baseline created at /,
/^\s*baseline\.go:\d+: the baseline file .* does not exist in the TypeScript submodule/,
/^\s*baseline\.go:\d+: the baseline file .* does not match the reference in the TypeScript submodule/,
];

// Check each output line that looks like an error message
// Error messages from Go tests typically contain ".go:" with a line number
const errorLines = outputs.filter(line => /^\s*\w+\.go:\d+:/.test(line));

// If there are no error lines, it's a real failure.
// If all error lines match baseline patterns, it's a baseline-only failure
const isBaselineOnlyFailure = errorLines.length > 0 &&
errorLines.every(line => baselineMessagePatterns.some(pattern => pattern.test(line)));

if (!isBaselineOnlyFailure) {
failingTests.push(event.Test);
}
}
}
catch (e) {
// Not JSON, possibly stderr or other output - ignore
}
});

testProcess.stderr.on("data", data => {
// Check stderr for panics too
const output = data.toString();
allOutputs.push(output);
if (/^panic/m.test(output)) {
hadPanic = true;
}
});

await new Promise<void>((resolve, reject) => {
testProcess.on("close", code => {
if (hadPanic) {
reject(new Error("Unrecovered panic detected in tests\n" + allOutputs.join("")));
return;
}

fs.writeFileSync(failingTestsPath, failingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
fs.writeFileSync(crashingTestsPath, crashingTests.sort((a, b) => a.localeCompare(b, "en-US")).join("\n") + "\n", "utf-8");
resolve();
});

testProcess.on("error", error => {
reject(error);
});
});
}

main();
main().catch(error => {
console.error("Error:", error);
process.exit(1);
});
5 changes: 1 addition & 4 deletions pkg/ls/folding.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,7 @@ func (l *LanguageService) addRegionOutliningSpans(sourceFile *ast.SourceFile) []
}

func visitNode(ctx context.Context, n *ast.Node, depthRemaining int, sourceFile *ast.SourceFile, l *LanguageService) []*lsproto.FoldingRange {
if depthRemaining == 0 {
return nil
}
if ctx.Err() != nil {
if n.Flags&ast.NodeFlagsReparsed != 0 || depthRemaining == 0 || ctx.Err() != nil {
return nil
}
foldingRange := make([]*lsproto.FoldingRange, 0, 40)
Expand Down
18 changes: 17 additions & 1 deletion pkg/ls/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,24 @@ func (l *LanguageService) getDocumentationFromDeclaration(c *checker.Checker, de
}
comments := tag.Comments()
if tag.Kind == ast.KindJSDocTag && tag.TagName().Text() == "example" {
b.WriteString("\n")
commentText := strings.TrimRight(getCommentText(comments), " \t\r\n")
if strings.HasPrefix(commentText, "<caption>") {
if captionEnd := strings.Index(commentText, "</caption>"); captionEnd > 0 {
b.WriteString(" — ")
b.WriteString(commentText[len("<caption>"):captionEnd])
commentText = commentText[captionEnd+len("</caption>"):]
// Trim leading blank lines from commentText
for {
s1 := strings.TrimLeft(commentText, " \t")
s2 := strings.TrimLeft(s1, "\r\n")
if len(s1) == len(s2) {
break
}
commentText = s2
}
}
}
b.WriteString("\n")
if len(commentText) > 6 && strings.HasPrefix(commentText, "```") && strings.HasSuffix(commentText, "```") && strings.Contains(commentText, "\n") {
b.WriteString(commentText)
b.WriteString("\n")
Expand Down
2 changes: 1 addition & 1 deletion pkg/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func ParseIsolatedEntityName(text string) *ast.EntityName {

func (p *Parser) initializeState(opts ast.SourceFileParseOptions, sourceText string, scriptKind core.ScriptKind) {
if scriptKind == core.ScriptKindUnknown {
panic("ScriptKind must be specified when parsing source files.")
panic("ScriptKind must be specified when parsing source file: " + opts.FileName)
}

if p.scanner == nil {
Expand Down
Loading