Skip to content

Commit dd49c28

Browse files
committed
Merge pull request microsoft#8014 from Microsoft/strictBlockScopeFunction
Make function block scoped in strict mode and report error in es5 for block scope level declaration of function
2 parents 146e994 + 163615b commit dd49c28

File tree

41 files changed

+831
-326
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+831
-326
lines changed

src/compiler/binder.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ namespace ts {
104104
function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
105105
let file: SourceFile;
106106
let options: CompilerOptions;
107+
let languageVersion: ScriptTarget;
107108
let parent: Node;
108109
let container: Node;
109110
let blockScopeContainer: Node;
@@ -136,6 +137,7 @@ namespace ts {
136137
function bindSourceFile(f: SourceFile, opts: CompilerOptions) {
137138
file = f;
138139
options = opts;
140+
languageVersion = getEmitScriptTarget(options);
139141
inStrictMode = !!file.externalModuleIndicator;
140142
classifiableNames = {};
141143

@@ -149,6 +151,7 @@ namespace ts {
149151

150152
file = undefined;
151153
options = undefined;
154+
languageVersion = undefined;
152155
parent = undefined;
153156
container = undefined;
154157
blockScopeContainer = undefined;
@@ -1125,6 +1128,35 @@ namespace ts {
11251128
}
11261129
}
11271130

1131+
function getStrictModeBlockScopeFunctionDeclarationMessage(node: Node) {
1132+
// Provide specialized messages to help the user understand why we think they're in
1133+
// strict mode.
1134+
if (getContainingClass(node)) {
1135+
return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Class_definitions_are_automatically_in_strict_mode;
1136+
}
1137+
1138+
if (file.externalModuleIndicator) {
1139+
return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5_Modules_are_automatically_in_strict_mode;
1140+
}
1141+
1142+
return Diagnostics.Function_declarations_are_not_allowed_inside_blocks_in_strict_mode_when_targeting_ES3_or_ES5;
1143+
}
1144+
1145+
function checkStrictModeFunctionDeclaration(node: FunctionDeclaration) {
1146+
if (languageVersion < ScriptTarget.ES6) {
1147+
// Report error if function is not top level function declaration
1148+
if (blockScopeContainer.kind !== SyntaxKind.SourceFile &&
1149+
blockScopeContainer.kind !== SyntaxKind.ModuleDeclaration &&
1150+
!isFunctionLike(blockScopeContainer)) {
1151+
// We check first if the name is inside class declaration or class expression; if so give explicit message
1152+
// otherwise report generic error message.
1153+
const errorSpan = getErrorSpanForNode(file, node);
1154+
file.bindDiagnostics.push(createFileDiagnostic(file, errorSpan.start, errorSpan.length,
1155+
getStrictModeBlockScopeFunctionDeclarationMessage(node)));
1156+
}
1157+
}
1158+
}
1159+
11281160
function checkStrictModeNumericLiteral(node: LiteralExpression) {
11291161
if (inStrictMode && node.isOctalLiteral) {
11301162
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode));
@@ -1640,7 +1672,13 @@ namespace ts {
16401672
}
16411673

16421674
checkStrictModeFunctionName(<FunctionDeclaration>node);
1643-
return declareSymbolAndAddToSymbolTable(<Declaration>node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
1675+
if (inStrictMode) {
1676+
checkStrictModeFunctionDeclaration(node);
1677+
return bindBlockScopedDeclaration(node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
1678+
}
1679+
else {
1680+
return declareSymbolAndAddToSymbolTable(<Declaration>node, SymbolFlags.Function, SymbolFlags.FunctionExcludes);
1681+
}
16441682
}
16451683

16461684
function bindFunctionExpression(node: FunctionExpression) {

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,18 @@
811811
"category": "Error",
812812
"code": 1249
813813
},
814+
"Function declarations are not allowed inside blocks in strict mode when targeting 'ES3' or 'ES5'.": {
815+
"category": "Error",
816+
"code": 1250
817+
},
818+
"Function declarations are not allowed inside blocks in strict mode when targeting 'ES3' or 'ES5'. Class definitions are automatically in strict mode.": {
819+
"category": "Error",
820+
"code": 1251
821+
},
822+
"Function declarations are not allowed inside blocks in strict mode when targeting 'ES3' or 'ES5'. Modules are automatically in strict mode.": {
823+
"category": "Error",
824+
"code": 1252
825+
},
814826
"'with' statements are not allowed in an async function block.": {
815827
"category": "Error",
816828
"code": 1300

src/harness/compilerRunner.ts

Lines changed: 105 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -256,125 +256,128 @@ class CompilerBaselineRunner extends RunnerBase {
256256
}
257257

258258
// NEWTODO: Type baselines
259-
if (result.errors.length === 0) {
260-
// The full walker simulates the types that you would get from doing a full
261-
// compile. The pull walker simulates the types you get when you just do
262-
// a type query for a random node (like how the LS would do it). Most of the
263-
// time, these will be the same. However, occasionally, they can be different.
264-
// Specifically, when the compiler internally depends on symbol IDs to order
265-
// things, then we may see different results because symbols can be created in a
266-
// different order with 'pull' operations, and thus can produce slightly differing
267-
// output.
268-
//
269-
// For example, with a full type check, we may see a type displayed as: number | string
270-
// But with a pull type check, we may see it as: string | number
271-
//
272-
// These types are equivalent, but depend on what order the compiler observed
273-
// certain parts of the program.
274-
275-
const program = result.program;
276-
const allFiles = toBeCompiled.concat(otherFiles).filter(file => !!program.getSourceFile(file.unitName));
277-
278-
const fullWalker = new TypeWriterWalker(program, /*fullTypeCheck*/ true);
279-
280-
const fullResults: ts.Map<TypeWriterResult[]> = {};
281-
const pullResults: ts.Map<TypeWriterResult[]> = {};
282-
283-
for (const sourceFile of allFiles) {
284-
fullResults[sourceFile.unitName] = fullWalker.getTypeAndSymbols(sourceFile.unitName);
285-
pullResults[sourceFile.unitName] = fullWalker.getTypeAndSymbols(sourceFile.unitName);
286-
}
259+
if (result.errors.length !== 0) {
260+
return;
261+
}
287262

288-
// Produce baselines. The first gives the types for all expressions.
289-
// The second gives symbols for all identifiers.
290-
let e1: Error, e2: Error;
291-
try {
292-
checkBaseLines(/*isSymbolBaseLine*/ false);
293-
}
294-
catch (e) {
295-
e1 = e;
296-
}
263+
// The full walker simulates the types that you would get from doing a full
264+
// compile. The pull walker simulates the types you get when you just do
265+
// a type query for a random node (like how the LS would do it). Most of the
266+
// time, these will be the same. However, occasionally, they can be different.
267+
// Specifically, when the compiler internally depends on symbol IDs to order
268+
// things, then we may see different results because symbols can be created in a
269+
// different order with 'pull' operations, and thus can produce slightly differing
270+
// output.
271+
//
272+
// For example, with a full type check, we may see a type displayed as: number | string
273+
// But with a pull type check, we may see it as: string | number
274+
//
275+
// These types are equivalent, but depend on what order the compiler observed
276+
// certain parts of the program.
277+
278+
const program = result.program;
279+
const allFiles = toBeCompiled.concat(otherFiles).filter(file => !!program.getSourceFile(file.unitName));
280+
281+
const fullWalker = new TypeWriterWalker(program, /*fullTypeCheck*/ true);
282+
283+
const fullResults: ts.Map<TypeWriterResult[]> = {};
284+
const pullResults: ts.Map<TypeWriterResult[]> = {};
285+
286+
for (const sourceFile of allFiles) {
287+
fullResults[sourceFile.unitName] = fullWalker.getTypeAndSymbols(sourceFile.unitName);
288+
pullResults[sourceFile.unitName] = fullWalker.getTypeAndSymbols(sourceFile.unitName);
289+
}
297290

298-
try {
299-
checkBaseLines(/*isSymbolBaseLine*/ true);
300-
}
301-
catch (e) {
302-
e2 = e;
303-
}
291+
// Produce baselines. The first gives the types for all expressions.
292+
// The second gives symbols for all identifiers.
293+
let e1: Error, e2: Error;
294+
try {
295+
checkBaseLines(/*isSymbolBaseLine*/ false);
296+
}
297+
catch (e) {
298+
e1 = e;
299+
}
304300

305-
if (e1 || e2) {
306-
throw e1 || e2;
307-
}
301+
try {
302+
checkBaseLines(/*isSymbolBaseLine*/ true);
303+
}
304+
catch (e) {
305+
e2 = e;
306+
}
308307

309-
return;
308+
if (e1 || e2) {
309+
throw e1 || e2;
310+
}
310311

311-
function checkBaseLines(isSymbolBaseLine: boolean) {
312-
const fullBaseLine = generateBaseLine(fullResults, isSymbolBaseLine);
313-
const pullBaseLine = generateBaseLine(pullResults, isSymbolBaseLine);
312+
return;
314313

315-
const fullExtension = isSymbolBaseLine ? ".symbols" : ".types";
316-
const pullExtension = isSymbolBaseLine ? ".symbols.pull" : ".types.pull";
314+
function checkBaseLines(isSymbolBaseLine: boolean) {
315+
const fullBaseLine = generateBaseLine(fullResults, isSymbolBaseLine);
316+
const pullBaseLine = generateBaseLine(pullResults, isSymbolBaseLine);
317317

318-
if (fullBaseLine !== pullBaseLine) {
319-
Harness.Baseline.runBaseline("Correct full information for " + fileName, justName.replace(/\.tsx?/, fullExtension), () => fullBaseLine);
320-
Harness.Baseline.runBaseline("Correct pull information for " + fileName, justName.replace(/\.tsx?/, pullExtension), () => pullBaseLine);
321-
}
322-
else {
323-
Harness.Baseline.runBaseline("Correct information for " + fileName, justName.replace(/\.tsx?/, fullExtension), () => fullBaseLine);
324-
}
318+
const fullExtension = isSymbolBaseLine ? ".symbols" : ".types";
319+
const pullExtension = isSymbolBaseLine ? ".symbols.pull" : ".types.pull";
320+
321+
if (fullBaseLine !== pullBaseLine) {
322+
Harness.Baseline.runBaseline("Correct full information for " + fileName, justName.replace(/\.tsx?/, fullExtension), () => fullBaseLine);
323+
Harness.Baseline.runBaseline("Correct pull information for " + fileName, justName.replace(/\.tsx?/, pullExtension), () => pullBaseLine);
325324
}
325+
else {
326+
Harness.Baseline.runBaseline("Correct information for " + fileName, justName.replace(/\.tsx?/, fullExtension), () => fullBaseLine);
327+
}
328+
}
326329

327-
function generateBaseLine(typeWriterResults: ts.Map<TypeWriterResult[]>, isSymbolBaseline: boolean): string {
328-
const typeLines: string[] = [];
329-
const typeMap: { [fileName: string]: { [lineNum: number]: string[]; } } = {};
330+
function generateBaseLine(typeWriterResults: ts.Map<TypeWriterResult[]>, isSymbolBaseline: boolean): string {
331+
const typeLines: string[] = [];
332+
const typeMap: { [fileName: string]: { [lineNum: number]: string[]; } } = {};
330333

331-
allFiles.forEach(file => {
332-
const codeLines = file.content.split("\n");
333-
typeWriterResults[file.unitName].forEach(result => {
334-
if (isSymbolBaseline && !result.symbol) {
335-
return;
336-
}
334+
allFiles.forEach(file => {
335+
const codeLines = file.content.split("\n");
336+
typeWriterResults[file.unitName].forEach(result => {
337+
if (isSymbolBaseline && !result.symbol) {
338+
return;
339+
}
337340

338-
const typeOrSymbolString = isSymbolBaseline ? result.symbol : result.type;
339-
const formattedLine = result.sourceText.replace(/\r?\n/g, "") + " : " + typeOrSymbolString;
340-
if (!typeMap[file.unitName]) {
341-
typeMap[file.unitName] = {};
342-
}
341+
const typeOrSymbolString = isSymbolBaseline ? result.symbol : result.type;
342+
const formattedLine = result.sourceText.replace(/\r?\n/g, "") + " : " + typeOrSymbolString;
343+
if (!typeMap[file.unitName]) {
344+
typeMap[file.unitName] = {};
345+
}
343346

344-
let typeInfo = [formattedLine];
345-
const existingTypeInfo = typeMap[file.unitName][result.line];
346-
if (existingTypeInfo) {
347-
typeInfo = existingTypeInfo.concat(typeInfo);
348-
}
349-
typeMap[file.unitName][result.line] = typeInfo;
350-
});
351-
352-
typeLines.push("=== " + file.unitName + " ===\r\n");
353-
for (let i = 0; i < codeLines.length; i++) {
354-
const currentCodeLine = codeLines[i];
355-
typeLines.push(currentCodeLine + "\r\n");
356-
if (typeMap[file.unitName]) {
357-
const typeInfo = typeMap[file.unitName][i];
358-
if (typeInfo) {
359-
typeInfo.forEach(ty => {
360-
typeLines.push(">" + ty + "\r\n");
361-
});
362-
if (i + 1 < codeLines.length && (codeLines[i + 1].match(/^\s*[{|}]\s*$/) || codeLines[i + 1].trim() === "")) {
363-
}
364-
else {
365-
typeLines.push("\r\n");
366-
}
347+
let typeInfo = [formattedLine];
348+
const existingTypeInfo = typeMap[file.unitName][result.line];
349+
if (existingTypeInfo) {
350+
typeInfo = existingTypeInfo.concat(typeInfo);
351+
}
352+
typeMap[file.unitName][result.line] = typeInfo;
353+
});
354+
355+
typeLines.push("=== " + file.unitName + " ===\r\n");
356+
for (let i = 0; i < codeLines.length; i++) {
357+
const currentCodeLine = codeLines[i];
358+
typeLines.push(currentCodeLine + "\r\n");
359+
if (typeMap[file.unitName]) {
360+
const typeInfo = typeMap[file.unitName][i];
361+
if (typeInfo) {
362+
typeInfo.forEach(ty => {
363+
typeLines.push(">" + ty + "\r\n");
364+
});
365+
if (i + 1 < codeLines.length && (codeLines[i + 1].match(/^\s*[{|}]\s*$/) || codeLines[i + 1].trim() === "")) {
366+
}
367+
else {
368+
typeLines.push("\r\n");
367369
}
368-
}
369-
else {
370-
typeLines.push("No type information for this code.");
371370
}
372371
}
373-
});
372+
else {
373+
typeLines.push("No type information for this code.");
374+
}
375+
}
376+
});
374377

375-
return typeLines.join("");
376-
}
378+
return typeLines.join("");
377379
}
380+
378381
});
379382
});
380383
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//// [blockScopedFunctionDeclarationES5.ts]
2+
if (true) {
3+
function foo() { }
4+
foo();
5+
}
6+
foo();
7+
8+
//// [blockScopedFunctionDeclarationES5.js]
9+
if (true) {
10+
function foo() { }
11+
foo();
12+
}
13+
foo();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
=== tests/cases/compiler/blockScopedFunctionDeclarationES5.ts ===
2+
if (true) {
3+
function foo() { }
4+
>foo : Symbol(foo, Decl(blockScopedFunctionDeclarationES5.ts, 0, 11))
5+
6+
foo();
7+
>foo : Symbol(foo, Decl(blockScopedFunctionDeclarationES5.ts, 0, 11))
8+
}
9+
foo();
10+
>foo : Symbol(foo, Decl(blockScopedFunctionDeclarationES5.ts, 0, 11))
11+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
=== tests/cases/compiler/blockScopedFunctionDeclarationES5.ts ===
2+
if (true) {
3+
>true : boolean
4+
5+
function foo() { }
6+
>foo : () => void
7+
8+
foo();
9+
>foo() : void
10+
>foo : () => void
11+
}
12+
foo();
13+
>foo() : void
14+
>foo : () => void
15+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//// [blockScopedFunctionDeclarationES6.ts]
2+
if (true) {
3+
function foo() { }
4+
foo();
5+
}
6+
foo();
7+
8+
//// [blockScopedFunctionDeclarationES6.js]
9+
if (true) {
10+
function foo() { }
11+
foo();
12+
}
13+
foo();
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
=== tests/cases/compiler/blockScopedFunctionDeclarationES6.ts ===
2+
if (true) {
3+
function foo() { }
4+
>foo : Symbol(foo, Decl(blockScopedFunctionDeclarationES6.ts, 0, 11))
5+
6+
foo();
7+
>foo : Symbol(foo, Decl(blockScopedFunctionDeclarationES6.ts, 0, 11))
8+
}
9+
foo();
10+
>foo : Symbol(foo, Decl(blockScopedFunctionDeclarationES6.ts, 0, 11))
11+

0 commit comments

Comments
 (0)