Skip to content

Commit

Permalink
feat!: use ESTree directive property when searching for `"use stric…
Browse files Browse the repository at this point in the history
…t"` (#118)

* feat!: use ESTree `directive` property when searching for `"use strict"`

Fixes #117

* add tests with functions
  • Loading branch information
mdjermanovic committed Jan 4, 2024
1 parent 42ef7a9 commit 23fe81f
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 39 deletions.
2 changes: 0 additions & 2 deletions lib/index.js
Expand Up @@ -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']
Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions lib/scope-manager.js
Expand Up @@ -53,10 +53,6 @@ class ScopeManager {
this.__declaredVariables = new WeakMap();
}

__useDirective() {
return this.__options.directive;
}

__isOptimistic() {
return this.__options.optimistic;
}
Expand Down
53 changes: 20 additions & 33 deletions lib/scope.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;

/**
Expand Down
180 changes: 180 additions & 0 deletions tests/use-strict.js
Expand Up @@ -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);
});
});
});

0 comments on commit 23fe81f

Please sign in to comment.