Skip to content

Commit 701f1af

Browse files
authoredJan 9, 2024
feat!: no-inner-declaration new default behaviour and option (#17885)
* feat: no-inner-declaration new default behaviour and option * feat!: removed no-inner-declarations from recommended * docs: added legacy option * feat!: updated the legacy option * feat!: added more test and updated docs * feat: no-inner-declaration new default behaviour and option * feat!: removed no-inner-declarations from recommended * docs: added legacy option * feat!: updated the legacy option * feat!: added more test and updated docs * added test for module * remove non-strict report for variablDeclarations * change legacy option to blockScopedFunctions * check upper scope * update docs and tests * update docs * add functions option in docs
1 parent b4e0503 commit 701f1af

File tree

3 files changed

+334
-23
lines changed

3 files changed

+334
-23
lines changed
 

‎docs/src/rules/no-inner-declarations.md

+149-10
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ function anotherThing() {
3030
}
3131
```
3232

33+
In ES6, [block-level functions](https://leanpub.com/understandinges6/read#leanpub-auto-block-level-functions) (functions declared inside a block) are limited to the scope of the block they are declared in and outside of the block scope they can't be accessed and called, but only when the code is in strict mode (code with `"use strict"` tag or ESM modules). In non-strict mode, they can be accessed and called outside of the block scope.
34+
35+
```js
36+
"use strict";
37+
38+
if (test) {
39+
function doSomething () { }
40+
41+
doSomething(); // no error
42+
}
43+
44+
doSomething(); // error
45+
```
46+
3347
A variable declaration is permitted anywhere a statement can go, even nested deeply inside other blocks. This is often undesirable due to variable hoisting, and moving declarations to the root of the program or function body can increase clarity. Note that [block bindings](https://leanpub.com/understandinges6/read#leanpub-auto-block-bindings) (`let`, `const`) are not hoisted and therefore they are not affected by this rule.
3448

3549
```js
@@ -65,10 +79,11 @@ This rule requires that function declarations and, optionally, variable declarat
6579

6680
## Options
6781

68-
This rule has a string option:
82+
This rule has a string and an object option:
6983

7084
* `"functions"` (default) disallows `function` declarations in nested blocks
7185
* `"both"` disallows `function` and `var` declarations in nested blocks
86+
* `{ blockScopedFunctions: "allow" }` (default) this option allows `function` declarations in nested blocks when code is in strict mode (code with `"use strict"` tag or ESM modules) and `languageOptions.ecmaVersion` is set to `2015` or above. This option can be disabled by setting it to `"disallow"`.
7287

7388
### functions
7489

@@ -79,6 +94,8 @@ Examples of **incorrect** code for this rule with the default `"functions"` opti
7994
```js
8095
/*eslint no-inner-declarations: "error"*/
8196

97+
// script, non-strict code
98+
8299
if (test) {
83100
function doSomething() { }
84101
}
@@ -90,14 +107,6 @@ function doSomethingElse() {
90107
}
91108

92109
if (foo) function f(){}
93-
94-
class C {
95-
static {
96-
if (test) {
97-
function doSomething() { }
98-
}
99-
}
100-
}
101110
```
102111

103112
:::
@@ -115,6 +124,14 @@ function doSomethingElse() {
115124
function doAnotherThing() { }
116125
}
117126

127+
function doSomethingElse() {
128+
"use strict";
129+
130+
if (test) {
131+
function doAnotherThing() { }
132+
}
133+
}
134+
118135
class C {
119136
static {
120137
function doSomething() { }
@@ -195,6 +212,128 @@ class C {
195212

196213
:::
197214

215+
### blockScopedFunctions
216+
217+
Example of **incorrect** code for this rule with `{ blockScopedFunctions: "disallow" }` option with `ecmaVersion: 2015`:
218+
219+
::: incorrect { "sourceType": "script", "ecmaVersion": 2015 }
220+
221+
```js
222+
/*eslint no-inner-declarations: ["error", "functions", { blockScopedFunctions: "disallow" }]*/
223+
224+
// non-strict code
225+
226+
if (test) {
227+
function doSomething() { }
228+
}
229+
230+
function doSomething() {
231+
if (test) {
232+
function doSomethingElse() { }
233+
}
234+
}
235+
236+
// strict code
237+
238+
function foo() {
239+
"use strict";
240+
241+
if (test) {
242+
function bar() { }
243+
}
244+
}
245+
```
246+
247+
:::
248+
249+
Example of **correct** code for this rule with `{ blockScopedFunctions: "disallow" }` option with `ecmaVersion: 2015`:
250+
251+
::: correct { "sourceType": "script", "ecmaVersion": 2015 }
252+
253+
```js
254+
/*eslint no-inner-declarations: ["error", "functions", { blockScopedFunctions: "disallow" }]*/
255+
256+
function doSomething() { }
257+
258+
function doSomething() {
259+
function doSomethingElse() { }
260+
}
261+
```
262+
263+
:::
264+
265+
Example of **correct** code for this rule with `{ blockScopedFunctions: "allow" }` option with `ecmaVersion: 2015`:
266+
267+
::: correct { "sourceType": "script", "ecmaVersion": 2015 }
268+
269+
```js
270+
/*eslint no-inner-declarations: ["error", "functions", { blockScopedFunctions: "allow" }]*/
271+
272+
"use strict";
273+
274+
if (test) {
275+
function doSomething() { }
276+
}
277+
278+
function doSomething() {
279+
if (test) {
280+
function doSomethingElse() { }
281+
}
282+
}
283+
284+
// OR
285+
286+
function foo() {
287+
"use strict";
288+
289+
if (test) {
290+
function bar() { }
291+
}
292+
}
293+
```
294+
295+
:::
296+
297+
`ESM modules` and both `class` declarations and expressions are always in strict mode.
298+
299+
::: correct { "sourceType": "module" }
300+
301+
```js
302+
/*eslint no-inner-declarations: ["error", "functions", { blockScopedFunctions: "allow" }]*/
303+
304+
if (test) {
305+
function doSomething() { }
306+
}
307+
308+
function doSomethingElse() {
309+
if (test) {
310+
function doAnotherThing() { }
311+
}
312+
}
313+
314+
class Some {
315+
static {
316+
if (test) {
317+
function doSomething() { }
318+
}
319+
}
320+
}
321+
322+
const C = class {
323+
static {
324+
if (test) {
325+
function doSomething() { }
326+
}
327+
}
328+
}
329+
```
330+
331+
:::
332+
198333
## When Not To Use It
199334

200-
The function declaration portion rule will be rendered obsolete when [block-scoped functions](https://bugzilla.mozilla.org/show_bug.cgi?id=585536) land in ES6, but until then, it should be left on to enforce valid constructions. Disable checking variable declarations when using [block-scoped-var](block-scoped-var) or if declaring variables in nested blocks is acceptable despite hoisting.
335+
By default, this rule disallows inner function declarations only in contexts where their behavior is unspecified and thus inconsistent (pre-ES6 environments) or legacy semantics apply (non-strict mode code). If your code targets pre-ES6 environments or is not in strict mode, you should enable this rule to prevent unexpected behavior.
336+
337+
In ES6+ environments, in strict mode code, the behavior of inner function declarations is well-defined and consistent - they are always block-scoped. If your code targets only ES6+ environments and is in strict mode (ES modules, or code with `"use strict"` directives) then there is no need to enable this rule unless you want to disallow inner functions as a stylistic choice, in which case you should enable this rule with the option `blockScopedFunctions: "disallow"`.
338+
339+
Disable checking variable declarations when using [block-scoped-var](block-scoped-var) or if declaring variables in nested blocks is acceptable despite hoisting.

‎lib/rules/no-inner-declarations.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ module.exports = {
5656
schema: [
5757
{
5858
enum: ["functions", "both"]
59+
},
60+
{
61+
type: "object",
62+
properties: {
63+
blockScopedFunctions: {
64+
enum: ["allow", "disallow"]
65+
}
66+
},
67+
additionalProperties: false
5968
}
6069
],
6170

@@ -66,6 +75,10 @@ module.exports = {
6675

6776
create(context) {
6877

78+
const sourceCode = context.sourceCode;
79+
const ecmaVersion = context.languageOptions.ecmaVersion;
80+
const blockScopedFunctions = context.options[1]?.blockScopedFunctions ?? "allow";
81+
6982
/**
7083
* Ensure that a given node is at a program or function body's root.
7184
* @param {ASTNode} node Declaration node to check.
@@ -97,7 +110,15 @@ module.exports = {
97110

98111
return {
99112

100-
FunctionDeclaration: check,
113+
FunctionDeclaration(node) {
114+
const isInStrictCode = sourceCode.getScope(node).upper.isStrict;
115+
116+
if (blockScopedFunctions === "allow" && ecmaVersion >= 2015 && isInStrictCode) {
117+
return;
118+
}
119+
120+
check(node);
121+
},
101122
VariableDeclaration(node) {
102123
if (context.options[0] === "both" && node.kind === "var") {
103124
check(node);

‎tests/lib/rules/no-inner-declarations.js

+163-12
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,36 @@ ruleTester.run("no-inner-declarations", rule, {
9898
code: "class C { static { var x; } }",
9999
options: ["both"],
100100
languageOptions: { ecmaVersion: 2022 }
101+
},
102+
{
103+
code: "'use strict' \n if (test) { function doSomething() { } }",
104+
options: ["functions", { blockScopedFunctions: "allow" }],
105+
languageOptions: { ecmaVersion: 2022 }
106+
},
107+
{
108+
code: "'use strict' \n if (test) { function doSomething() { } }",
109+
options: ["functions"],
110+
languageOptions: { ecmaVersion: 2022 }
111+
},
112+
{
113+
code: "function foo() {'use strict' \n if (test) { function doSomething() { } } }",
114+
options: ["functions", { blockScopedFunctions: "allow" }],
115+
languageOptions: { ecmaVersion: 6 }
116+
},
117+
{
118+
code: "function foo() { { function bar() { } } }",
119+
options: ["functions", { blockScopedFunctions: "allow" }],
120+
languageOptions: { ecmaVersion: 2022, sourceType: "module" }
121+
},
122+
{
123+
code: "class C { method() { if(test) { function somethingElse() { } } } }",
124+
options: ["functions", { blockScopedFunctions: "allow" }],
125+
languageOptions: { ecmaVersion: 2022 }
126+
},
127+
{
128+
code: "const C = class { method() { if(test) { function somethingElse() { } } } }",
129+
options: ["functions", { blockScopedFunctions: "allow" }],
130+
languageOptions: { ecmaVersion: 2022 }
101131
}
102132
],
103133

@@ -155,7 +185,7 @@ ruleTester.run("no-inner-declarations", rule, {
155185
type: "VariableDeclaration"
156186
}]
157187
}, {
158-
code: "if (foo) function f(){ if(bar) var a; } ",
188+
code: "if (foo) function f(){ if(bar) var a; }",
159189
options: ["both"],
160190
errors: [{
161191
messageId: "moveDeclToRoot",
@@ -220,7 +250,7 @@ ruleTester.run("no-inner-declarations", rule, {
220250
}]
221251
},
222252
{
223-
code: "if (foo){ var a; }",
253+
code: "if (foo) { var a; }",
224254
options: ["both"],
225255
errors: [{
226256
messageId: "moveDeclToRoot",
@@ -295,7 +325,8 @@ ruleTester.run("no-inner-declarations", rule, {
295325
},
296326
type: "VariableDeclaration"
297327
}]
298-
}, {
328+
},
329+
{
299330
code: "class C { method() { if(test) { var foo; } } }",
300331
options: ["both"],
301332
languageOptions: { ecmaVersion: 6 },
@@ -307,10 +338,24 @@ ruleTester.run("no-inner-declarations", rule, {
307338
},
308339
type: "VariableDeclaration"
309340
}]
310-
}, {
311-
code: "class C { static { if (test) { function foo() {} } } }",
341+
},
342+
{
343+
code: "class C { static { if (test) { var foo; } } }",
312344
options: ["both"],
313345
languageOptions: { ecmaVersion: 2022 },
346+
errors: [{
347+
messageId: "moveDeclToRoot",
348+
data: {
349+
type: "variable",
350+
body: "class static block body"
351+
},
352+
type: "VariableDeclaration"
353+
}]
354+
},
355+
{
356+
code: "class C { static { if (test) { function foo() {} } } }",
357+
options: ["both", { blockScopedFunctions: "disallow" }],
358+
languageOptions: { ecmaVersion: 2022 },
314359
errors: [{
315360
messageId: "moveDeclToRoot",
316361
data: {
@@ -319,8 +364,9 @@ ruleTester.run("no-inner-declarations", rule, {
319364
},
320365
type: "FunctionDeclaration"
321366
}]
322-
}, {
323-
code: "class C { static { if (test) { var foo; } } }",
367+
},
368+
{
369+
code: "class C { static { if (test) { if (anotherTest) { var foo; } } } }",
324370
options: ["both"],
325371
languageOptions: { ecmaVersion: 2022 },
326372
errors: [{
@@ -331,17 +377,122 @@ ruleTester.run("no-inner-declarations", rule, {
331377
},
332378
type: "VariableDeclaration"
333379
}]
334-
}, {
335-
code: "class C { static { if (test) { if (anotherTest) { var foo; } } } }",
380+
},
381+
{
382+
code: "if (test) { function doSomething() { } }",
383+
options: ["both", { blockScopedFunctions: "allow" }],
384+
languageOptions: { ecmaVersion: 5 },
385+
errors: [{
386+
messageId: "moveDeclToRoot",
387+
data: {
388+
type: "function",
389+
body: "program"
390+
},
391+
type: "FunctionDeclaration"
392+
}]
393+
},
394+
{
395+
code: "if (test) { function doSomething() { } }",
396+
options: ["both", { blockScopedFunctions: "disallow" }],
397+
languageOptions: { ecmaVersion: 2022 },
398+
errors: [{
399+
messageId: "moveDeclToRoot",
400+
data: {
401+
type: "function",
402+
body: "program"
403+
},
404+
type: "FunctionDeclaration"
405+
}]
406+
},
407+
{
408+
code: "'use strict' \n if (test) { function doSomething() { } }",
409+
options: ["both", { blockScopedFunctions: "disallow" }],
410+
languageOptions: { ecmaVersion: 2022 },
411+
errors: [{
412+
messageId: "moveDeclToRoot",
413+
data: {
414+
type: "function",
415+
body: "program"
416+
},
417+
type: "FunctionDeclaration"
418+
}]
419+
},
420+
{
421+
code: "'use strict' \n if (test) { function doSomething() { } }",
422+
options: ["both", { blockScopedFunctions: "disallow" }],
423+
languageOptions: { ecmaVersion: 5 },
424+
errors: [{
425+
messageId: "moveDeclToRoot",
426+
data: {
427+
type: "function",
428+
body: "program"
429+
},
430+
type: "FunctionDeclaration"
431+
}]
432+
},
433+
{
434+
code: "'use strict' \n if (test) { function doSomething() { } }",
435+
options: ["both", { blockScopedFunctions: "allow" }],
436+
languageOptions: { ecmaVersion: 5 },
437+
errors: [{
438+
messageId: "moveDeclToRoot",
439+
data: {
440+
type: "function",
441+
body: "program"
442+
},
443+
type: "FunctionDeclaration"
444+
}]
445+
},
446+
{
447+
code: "function foo() {'use strict' \n { function bar() { } } }",
448+
options: ["both", { blockScopedFunctions: "disallow" }],
449+
languageOptions: { ecmaVersion: 2022 },
450+
errors: [{
451+
messageId: "moveDeclToRoot",
452+
data: {
453+
type: "function",
454+
body: "function body"
455+
},
456+
type: "FunctionDeclaration"
457+
}]
458+
},
459+
{
460+
code: "function foo() {'use strict' \n { function bar() { } } }",
461+
options: ["both", { blockScopedFunctions: "disallow" }],
462+
languageOptions: { ecmaVersion: 5 },
463+
errors: [{
464+
messageId: "moveDeclToRoot",
465+
data: {
466+
type: "function",
467+
body: "function body"
468+
},
469+
type: "FunctionDeclaration"
470+
}]
471+
},
472+
{
473+
code: "function doSomething() { 'use strict' \n do { function somethingElse() { } } while (test); }",
474+
options: ["both", { blockScopedFunctions: "disallow" }],
475+
languageOptions: { ecmaVersion: 5 },
476+
errors: [{
477+
messageId: "moveDeclToRoot",
478+
data: {
479+
type: "function",
480+
body: "function body"
481+
},
482+
type: "FunctionDeclaration"
483+
}]
484+
},
485+
{
486+
code: "{ function foo () {'use strict' \n console.log('foo called'); } }",
336487
options: ["both"],
337488
languageOptions: { ecmaVersion: 2022 },
338489
errors: [{
339490
messageId: "moveDeclToRoot",
340491
data: {
341-
type: "variable",
342-
body: "class static block body"
492+
type: "function",
493+
body: "program"
343494
},
344-
type: "VariableDeclaration"
495+
type: "FunctionDeclaration"
345496
}]
346497
}
347498
]

0 commit comments

Comments
 (0)
Please sign in to comment.