From 23fe81f5861ade17a2f17f9518fdde376dd2395f Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Thu, 4 Jan 2024 19:20:40 +0100 Subject: [PATCH] feat!: use ESTree `directive` property when searching for `"use strict"` (#118) * feat!: use ESTree `directive` property when searching for `"use strict"` Fixes #117 * add tests with functions --- lib/index.js | 2 - lib/scope-manager.js | 4 - lib/scope.js | 53 +++++-------- tests/use-strict.js | 180 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 39 deletions(-) diff --git a/lib/index.js b/lib/index.js index cd0678d..03ae395 100644 --- a/lib/index.js +++ b/lib/index.js @@ -63,7 +63,6 @@ import eslintScopeVersion from "./version.js"; function defaultOptions() { return { optimistic: false, - directive: false, nodejsScope: false, impliedStrict: false, sourceType: "script", // one of ['script', 'module', 'commonjs'] @@ -115,7 +114,6 @@ function updateDeeply(target, override) { * @param {espree.Tree} tree Abstract Syntax Tree * @param {Object} providedOptions Options that tailor the scope analysis * @param {boolean} [providedOptions.optimistic=false] the optimistic flag - * @param {boolean} [providedOptions.directive=false] the directive flag * @param {boolean} [providedOptions.ignoreEval=false] whether to check 'eval()' calls * @param {boolean} [providedOptions.nodejsScope=false] whether the whole * script is executed under node.js environment. When enabled, escope adds diff --git a/lib/scope-manager.js b/lib/scope-manager.js index d2270f1..f59eac0 100644 --- a/lib/scope-manager.js +++ b/lib/scope-manager.js @@ -53,10 +53,6 @@ class ScopeManager { this.__declaredVariables = new WeakMap(); } - __useDirective() { - return this.__options.directive; - } - __isOptimistic() { return this.__options.optimistic; } diff --git a/lib/scope.js b/lib/scope.js index 0619b90..4e5a732 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -39,10 +39,9 @@ const { Syntax } = estraverse; * @param {Scope} scope scope * @param {Block} block block * @param {boolean} isMethodDefinition is method definition - * @param {boolean} useDirective use directive * @returns {boolean} is strict scope */ -function isStrictScope(scope, block, isMethodDefinition, useDirective) { +function isStrictScope(scope, block, isMethodDefinition) { let body; // When upper scope is exists and strict, inner scope is also strict. @@ -82,41 +81,29 @@ function isStrictScope(scope, block, isMethodDefinition, useDirective) { return false; } - // Search 'use strict' directive. - if (useDirective) { - for (let i = 0, iz = body.body.length; i < iz; ++i) { - const stmt = body.body[i]; + // Search for a 'use strict' directive. + for (let i = 0, iz = body.body.length; i < iz; ++i) { + const stmt = body.body[i]; - if (stmt.type !== Syntax.DirectiveStatement) { - break; - } - if (stmt.raw === "\"use strict\"" || stmt.raw === "'use strict'") { - return true; - } + /* + * Check if the current statement is a directive. + * If it isn't, then we're past the directive prologue + * so stop the search because directives cannot + * appear after this point. + * + * Some parsers set `directive:null` on non-directive + * statements, so the `typeof` check is safer than + * checking for property existence. + */ + if (typeof stmt.directive !== "string") { + break; } - } else { - for (let i = 0, iz = body.body.length; i < iz; ++i) { - const stmt = body.body[i]; - if (stmt.type !== Syntax.ExpressionStatement) { - break; - } - const expr = stmt.expression; - - if (expr.type !== Syntax.Literal || typeof expr.value !== "string") { - break; - } - if (expr.raw !== null && expr.raw !== undefined) { - if (expr.raw === "\"use strict\"" || expr.raw === "'use strict'") { - return true; - } - } else { - if (expr.value === "use strict") { - return true; - } - } + if (stmt.directive === "use strict") { + return true; } } + return false; } @@ -264,7 +251,7 @@ class Scope { * @member {boolean} Scope#isStrict */ this.isStrict = scopeManager.isStrictModeSupported() - ? isStrictScope(this, block, isMethodDefinition, scopeManager.__useDirective()) + ? isStrictScope(this, block, isMethodDefinition) : false; /** diff --git a/tests/use-strict.js b/tests/use-strict.js index e7f6916..7621795 100644 --- a/tests/use-strict.js +++ b/tests/use-strict.js @@ -98,4 +98,184 @@ describe("'use strict' directives", () => { assertIsStrictRecursively(globalScope.childScopes[1], false); // function e() { ... } }); }); + + it("can be with single quotes at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + 'use strict'; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, true); + }); + }); + + it("can be without the semicolon at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "use strict" + foo() + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, true); + }); + }); + + it("can be anywhere in the directive prologue at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "foo"; + "use strict"; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, true); + }); + }); + + it("cannot be after the directive prologue at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + foo(); + "use strict"; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + }); + }); + + it("cannot contain escapes at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + "use \\strict"; + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + }); + }); + + it("cannot be parenthesized at the top level", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + ("use strict"); + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + }); + }); + + it("can be with single quotes in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + 'use strict'; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, true); + }); + }); + + it("can be without the semicolon in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + "use strict" + bar() + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, true); + }); + }); + + it("can be anywhere in the directive prologue in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + "foo"; + "use strict"; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, true); + }); + }); + + it("cannot be after the directive prologue in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + bar(); + "use strict"; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, false); + }); + }); + + it("cannot contain escapes in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + "use \\strict"; + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, false); + }); + }); + + it("cannot be parenthesized in a function", () => { + getSupportedEcmaVersions({ min: 5 }).forEach(ecmaVersion => { + const ast = espree.parse(` + function foo() { + ("use strict"); + } + `, { ecmaVersion, range: true }); + + const { globalScope } = analyze(ast, { ecmaVersion, childVisitorKeys: KEYS }); + + assert.strictEqual(globalScope.isStrict, false); + assert.strictEqual(globalScope.childScopes.length, 1); + assert.strictEqual(globalScope.childScopes[0].type, "function"); + assert.strictEqual(globalScope.childScopes[0].isStrict, false); + }); + }); });