From 5045f874e00ad0e6c358b2edbc34ea7f6a861d5d Mon Sep 17 00:00:00 2001 From: mukunda katta Date: Mon, 11 May 2026 15:35:08 -0700 Subject: [PATCH] Improve label reference diagnostics --- src/compiler/checker.ts | 65 +++++++++ src/compiler/diagnosticMessages.json | 4 + .../labelReferenceDiagnostics.errors.txt | 76 ++++++++++ .../reference/labelReferenceDiagnostics.js | 87 ++++++++++++ .../labelReferenceDiagnostics.symbols | 70 +++++++++ .../reference/labelReferenceDiagnostics.types | 134 ++++++++++++++++++ .../compiler/labelReferenceDiagnostics.ts | 49 +++++++ 7 files changed, 485 insertions(+) create mode 100644 tests/baselines/reference/labelReferenceDiagnostics.errors.txt create mode 100644 tests/baselines/reference/labelReferenceDiagnostics.js create mode 100644 tests/baselines/reference/labelReferenceDiagnostics.symbols create mode 100644 tests/baselines/reference/labelReferenceDiagnostics.types create mode 100644 tests/cases/compiler/labelReferenceDiagnostics.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..eddd499b2e868 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -53030,6 +53030,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { let current: Node = node; while (current) { if (isFunctionLikeOrClassStaticBlockDeclaration(current)) { + if (node.label && !hasContainingLabel(node, current)) { + const label = findLabelInCurrentFunction(node); + if (label && label.pos > node.pos) { + return grammarErrorOnNodeWithRelatedLabel(node, Diagnostics.A_label_cannot_be_referenced_prior_to_its_declared_location, label); + } + } + return grammarErrorOnNode(node, Diagnostics.Jump_target_cannot_cross_function_boundary); } @@ -53080,6 +53087,64 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } } + function hasContainingLabel(node: BreakOrContinueStatement, boundary: Node): boolean { + Debug.assert(!!node.label); + + for (let current = boundary.parent; current; current = current.parent) { + if ( + current.kind === SyntaxKind.LabeledStatement && + current.pos <= boundary.pos && + boundary.end <= current.end && + (current as LabeledStatement).label.escapedText === node.label.escapedText + ) { + return true; + } + } + + return false; + } + + function findLabelInCurrentFunction(node: BreakOrContinueStatement): LabeledStatement | undefined { + Debug.assert(!!node.label); + + for (let current: Node = node; current && !isFunctionLikeOrClassStaticBlockDeclaration(current); current = current.parent) { + if (!current.parent) { + break; + } + const statements = getStatementContainerStatements(current.parent); + if (statements) { + const label = find(statements, statement => statement.kind === SyntaxKind.LabeledStatement && (statement as LabeledStatement).label.escapedText === node.label!.escapedText) as LabeledStatement | undefined; + if (label) { + return label; + } + } + } + + return undefined; + } + + function getStatementContainerStatements(node: Node): NodeArray | undefined { + switch (node.kind) { + case SyntaxKind.SourceFile: + return (node as SourceFile).statements; + case SyntaxKind.Block: + case SyntaxKind.ModuleBlock: + return (node as Block | ModuleBlock).statements; + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + return (node as CaseOrDefaultClause).statements; + } + } + + function grammarErrorOnNodeWithRelatedLabel(node: BreakOrContinueStatement, message: DiagnosticMessage, label: LabeledStatement): boolean { + const sourceFile = getSourceFileOfNode(node); + if (!hasParseDiagnostics(sourceFile)) { + addRelatedInfo(error(node, message), createDiagnosticForNode(label.label, Diagnostics._0_is_declared_here, idText(label.label))); + return true; + } + return false; + } + function checkGrammarBindingElement(node: BindingElement) { if (node.dotDotDotToken) { const elements = node.parent.elements; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 17586e927d0ac..1b3710bb2d9ee 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -307,6 +307,10 @@ "category": "Error", "code": 1107 }, + "A label cannot be referenced prior to its declared location.": { + "category": "Error", + "code": 18001 + }, "A 'return' statement can only be used within a function body.": { "category": "Error", "code": 1108 diff --git a/tests/baselines/reference/labelReferenceDiagnostics.errors.txt b/tests/baselines/reference/labelReferenceDiagnostics.errors.txt new file mode 100644 index 0000000000000..f71ae2b9300e3 --- /dev/null +++ b/tests/baselines/reference/labelReferenceDiagnostics.errors.txt @@ -0,0 +1,76 @@ +labelReferenceDiagnostics.ts(2,5): error TS18001: A label cannot be referenced prior to its declared location. +labelReferenceDiagnostics.ts(8,5): error TS18001: A label cannot be referenced prior to its declared location. +labelReferenceDiagnostics.ts(14,5): error TS1107: Jump target cannot cross function boundary. +labelReferenceDiagnostics.ts(18,5): error TS1107: Jump target cannot cross function boundary. +labelReferenceDiagnostics.ts(23,9): error TS1107: Jump target cannot cross function boundary. +labelReferenceDiagnostics.ts(30,5): error TS1107: Jump target cannot cross function boundary. +labelReferenceDiagnostics.ts(37,9): error TS1107: Jump target cannot cross function boundary. +labelReferenceDiagnostics.ts(44,9): error TS1107: Jump target cannot cross function boundary. + + +==== labelReferenceDiagnostics.ts (8 errors) ==== + function breakToLaterLabel() { + break target; + ~~~~~~~~~~~~~ +!!! error TS18001: A label cannot be referenced prior to its declared location. +!!! related TS2728 labelReferenceDiagnostics.ts:3:5: 'target' is declared here. + target: + while (true) {} + } + + function continueToLaterLabel() { + continue target; + ~~~~~~~~~~~~~~~~ +!!! error TS18001: A label cannot be referenced prior to its declared location. +!!! related TS2728 labelReferenceDiagnostics.ts:9:5: 'target' is declared here. + target: + while (true) {} + } + + function breakToMissingLabel() { + break target; + ~~~~~~~~~~~~~ +!!! error TS1107: Jump target cannot cross function boundary. + } + + function breakToFunctionNameLabel() { + break breakToFunctionNameLabel; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS1107: Jump target cannot cross function boundary. + } + + function continueToMissingLabel() { + while (true) { + continue target; + ~~~~~~~~~~~~~~~~ +!!! error TS1107: Jump target cannot cross function boundary. + } + } + + function breakToNonEnclosingLabel() { + target: + console.log("target"); + break target; + ~~~~~~~~~~~~~ +!!! error TS1107: Jump target cannot cross function boundary. + } + + function continueToNonEnclosingLabel() { + target: + while (true) {} + while (true) { + continue target; + ~~~~~~~~~~~~~~~~ +!!! error TS1107: Jump target cannot cross function boundary. + } + } + + target: + while (true) { + function crossesFunctionBoundary() { + break target; + ~~~~~~~~~~~~~ +!!! error TS1107: Jump target cannot cross function boundary. + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/labelReferenceDiagnostics.js b/tests/baselines/reference/labelReferenceDiagnostics.js new file mode 100644 index 0000000000000..d44ef661eac09 --- /dev/null +++ b/tests/baselines/reference/labelReferenceDiagnostics.js @@ -0,0 +1,87 @@ +//// [tests/cases/compiler/labelReferenceDiagnostics.ts] //// + +//// [labelReferenceDiagnostics.ts] +function breakToLaterLabel() { + break target; + target: + while (true) {} +} + +function continueToLaterLabel() { + continue target; + target: + while (true) {} +} + +function breakToMissingLabel() { + break target; +} + +function breakToFunctionNameLabel() { + break breakToFunctionNameLabel; +} + +function continueToMissingLabel() { + while (true) { + continue target; + } +} + +function breakToNonEnclosingLabel() { + target: + console.log("target"); + break target; +} + +function continueToNonEnclosingLabel() { + target: + while (true) {} + while (true) { + continue target; + } +} + +target: +while (true) { + function crossesFunctionBoundary() { + break target; + } +} + + +//// [labelReferenceDiagnostics.js] +"use strict"; +function breakToLaterLabel() { + break target; + target: while (true) { } +} +function continueToLaterLabel() { + continue target; + target: while (true) { } +} +function breakToMissingLabel() { + break target; +} +function breakToFunctionNameLabel() { + break breakToFunctionNameLabel; +} +function continueToMissingLabel() { + while (true) { + continue target; + } +} +function breakToNonEnclosingLabel() { + target: console.log("target"); + break target; +} +function continueToNonEnclosingLabel() { + target: while (true) { } + while (true) { + continue target; + } +} +target: while (true) { + function crossesFunctionBoundary() { + break target; + } +} diff --git a/tests/baselines/reference/labelReferenceDiagnostics.symbols b/tests/baselines/reference/labelReferenceDiagnostics.symbols new file mode 100644 index 0000000000000..6c58e2ca29e39 --- /dev/null +++ b/tests/baselines/reference/labelReferenceDiagnostics.symbols @@ -0,0 +1,70 @@ +//// [tests/cases/compiler/labelReferenceDiagnostics.ts] //// + +=== labelReferenceDiagnostics.ts === +function breakToLaterLabel() { +>breakToLaterLabel : Symbol(breakToLaterLabel, Decl(labelReferenceDiagnostics.ts, 0, 0)) + + break target; + target: + while (true) {} +} + +function continueToLaterLabel() { +>continueToLaterLabel : Symbol(continueToLaterLabel, Decl(labelReferenceDiagnostics.ts, 4, 1)) + + continue target; + target: + while (true) {} +} + +function breakToMissingLabel() { +>breakToMissingLabel : Symbol(breakToMissingLabel, Decl(labelReferenceDiagnostics.ts, 10, 1)) + + break target; +} + +function breakToFunctionNameLabel() { +>breakToFunctionNameLabel : Symbol(breakToFunctionNameLabel, Decl(labelReferenceDiagnostics.ts, 14, 1)) + + break breakToFunctionNameLabel; +} + +function continueToMissingLabel() { +>continueToMissingLabel : Symbol(continueToMissingLabel, Decl(labelReferenceDiagnostics.ts, 18, 1)) + + while (true) { + continue target; + } +} + +function breakToNonEnclosingLabel() { +>breakToNonEnclosingLabel : Symbol(breakToNonEnclosingLabel, Decl(labelReferenceDiagnostics.ts, 24, 1)) + + target: + console.log("target"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + + break target; +} + +function continueToNonEnclosingLabel() { +>continueToNonEnclosingLabel : Symbol(continueToNonEnclosingLabel, Decl(labelReferenceDiagnostics.ts, 30, 1)) + + target: + while (true) {} + while (true) { + continue target; + } +} + +target: +while (true) { + function crossesFunctionBoundary() { +>crossesFunctionBoundary : Symbol(crossesFunctionBoundary, Decl(labelReferenceDiagnostics.ts, 41, 14)) + + break target; + } +} + diff --git a/tests/baselines/reference/labelReferenceDiagnostics.types b/tests/baselines/reference/labelReferenceDiagnostics.types new file mode 100644 index 0000000000000..ad569a0909a91 --- /dev/null +++ b/tests/baselines/reference/labelReferenceDiagnostics.types @@ -0,0 +1,134 @@ +//// [tests/cases/compiler/labelReferenceDiagnostics.ts] //// + +=== labelReferenceDiagnostics.ts === +function breakToLaterLabel() { +>breakToLaterLabel : () => void +> : ^^^^^^^^^^ + + break target; +>target : any +> : ^^^ + + target: +>target : any +> : ^^^ + + while (true) {} +>true : true +> : ^^^^ +} + +function continueToLaterLabel() { +>continueToLaterLabel : () => void +> : ^^^^^^^^^^ + + continue target; +>target : any +> : ^^^ + + target: +>target : any +> : ^^^ + + while (true) {} +>true : true +> : ^^^^ +} + +function breakToMissingLabel() { +>breakToMissingLabel : () => void +> : ^^^^^^^^^^ + + break target; +>target : any +> : ^^^ +} + +function breakToFunctionNameLabel() { +>breakToFunctionNameLabel : () => void +> : ^^^^^^^^^^ + + break breakToFunctionNameLabel; +>breakToFunctionNameLabel : any +> : ^^^ +} + +function continueToMissingLabel() { +>continueToMissingLabel : () => void +> : ^^^^^^^^^^ + + while (true) { +>true : true +> : ^^^^ + + continue target; +>target : any +> : ^^^ + } +} + +function breakToNonEnclosingLabel() { +>breakToNonEnclosingLabel : () => void +> : ^^^^^^^^^^ + + target: +>target : any +> : ^^^ + + console.log("target"); +>console.log("target") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"target" : "target" +> : ^^^^^^^^ + + break target; +>target : any +> : ^^^ +} + +function continueToNonEnclosingLabel() { +>continueToNonEnclosingLabel : () => void +> : ^^^^^^^^^^ + + target: +>target : any +> : ^^^ + + while (true) {} +>true : true +> : ^^^^ + + while (true) { +>true : true +> : ^^^^ + + continue target; +>target : any +> : ^^^ + } +} + +target: +>target : any +> : ^^^ + +while (true) { +>true : true +> : ^^^^ + + function crossesFunctionBoundary() { +>crossesFunctionBoundary : () => void +> : ^^^^^^^^^^ + + break target; +>target : any +> : ^^^ + } +} + diff --git a/tests/cases/compiler/labelReferenceDiagnostics.ts b/tests/cases/compiler/labelReferenceDiagnostics.ts new file mode 100644 index 0000000000000..fc58f2fb53f38 --- /dev/null +++ b/tests/cases/compiler/labelReferenceDiagnostics.ts @@ -0,0 +1,49 @@ +// @target: es2015 +// @allowUnusedLabels: true + +function breakToLaterLabel() { + break target; + target: + while (true) {} +} + +function continueToLaterLabel() { + continue target; + target: + while (true) {} +} + +function breakToMissingLabel() { + break target; +} + +function breakToFunctionNameLabel() { + break breakToFunctionNameLabel; +} + +function continueToMissingLabel() { + while (true) { + continue target; + } +} + +function breakToNonEnclosingLabel() { + target: + console.log("target"); + break target; +} + +function continueToNonEnclosingLabel() { + target: + while (true) {} + while (true) { + continue target; + } +} + +target: +while (true) { + function crossesFunctionBoundary() { + break target; + } +}