381
tests/platform-guard.unit.test.cjs
Normal file
381
tests/platform-guard.unit.test.cjs
Normal file
@@ -0,0 +1,381 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* platform-guard.unit.test.cjs
|
||||
*
|
||||
* Unit tests for eslint-rules/lib/platform-guard.cjs.
|
||||
* Verifies classifyPlatformTest and isWindowsExcludedNode shapes
|
||||
* using espree to parse code snippets into ASTs.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const espree = require('espree');
|
||||
const { Linter } = require('eslint');
|
||||
|
||||
const { classifyPlatformTest, isWindowsExcludedNode } = require('../eslint-rules/lib/platform-guard.cjs');
|
||||
|
||||
const PARSE_OPTIONS = {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'script',
|
||||
range: true,
|
||||
loc: true,
|
||||
tokens: true,
|
||||
comment: true,
|
||||
};
|
||||
|
||||
function parse(code) {
|
||||
return espree.parse(code, PARSE_OPTIONS);
|
||||
}
|
||||
|
||||
// ─── classifyPlatformTest ─────────────────────────────────────────────────────
|
||||
|
||||
describe('classifyPlatformTest', () => {
|
||||
test('process.platform === "win32" → windows', () => {
|
||||
const ast = parse(`process.platform === 'win32'`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('"win32" === process.platform (reversed) → windows', () => {
|
||||
const ast = parse(`'win32' === process.platform`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('process.platform !== "win32" → not-windows', () => {
|
||||
const ast = parse(`process.platform !== 'win32'`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'not-windows');
|
||||
});
|
||||
|
||||
test('os.platform() === "win32" → windows', () => {
|
||||
const ast = parse(`os.platform() === 'win32'`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('os.platform() !== "win32" → not-windows', () => {
|
||||
const ast = parse(`os.platform() !== 'win32'`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'not-windows');
|
||||
});
|
||||
|
||||
test('isWindows identifier → windows', () => {
|
||||
const ast = parse(`isWindows`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('IS_WINDOWS identifier → windows', () => {
|
||||
const ast = parse(`IS_WINDOWS`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('isWin identifier → windows', () => {
|
||||
const ast = parse(`isWin`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('onWindows identifier → windows', () => {
|
||||
const ast = parse(`onWindows`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'windows');
|
||||
});
|
||||
|
||||
test('!isWindows → not-windows', () => {
|
||||
const ast = parse(`!isWindows`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), 'not-windows');
|
||||
});
|
||||
|
||||
test('unrelated expression → null', () => {
|
||||
const ast = parse(`x === 'linux'`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), null);
|
||||
});
|
||||
|
||||
test('bare identifier "result" → null', () => {
|
||||
const ast = parse(`result`);
|
||||
const node = ast.body[0].expression;
|
||||
assert.strictEqual(classifyPlatformTest(node), null);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── isWindowsExcludedNode via Linter ────────────────────────────────────────
|
||||
// We use the Linter + a custom rule to get real sourceCode with ancestors.
|
||||
|
||||
/**
|
||||
* Build a mini rule that collects data about CallExpression nodes named
|
||||
* "assert.equal" and checks isWindowsExcludedNode on them.
|
||||
*/
|
||||
function buildCollectorRule() {
|
||||
return {
|
||||
create(context) {
|
||||
const sourceCode = context.sourceCode;
|
||||
const results = [];
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.name === 'assert' &&
|
||||
node.callee.property.name === 'equal'
|
||||
) {
|
||||
results.push(isWindowsExcludedNode(node, sourceCode));
|
||||
}
|
||||
},
|
||||
'Program:exit'() {
|
||||
context.report({ node: sourceCode.ast, messageId: 'result', data: { results: JSON.stringify(results) } });
|
||||
},
|
||||
};
|
||||
},
|
||||
meta: { type: 'suggestion', schema: [], messages: { result: '{{results}}' } },
|
||||
};
|
||||
}
|
||||
|
||||
function runCollector(code) {
|
||||
const linter = new Linter({ configType: 'flat' });
|
||||
const messages = linter.verify(
|
||||
code,
|
||||
[{ plugins: { t: { rules: { collector: buildCollectorRule() } } }, rules: { 't/collector': 'warn' }, languageOptions: { ecmaVersion: 2022, sourceType: 'script' } }],
|
||||
{ filename: 'tests/x.test.cjs' }
|
||||
);
|
||||
// The rule emits exactly one message (Program:exit) with results as JSON
|
||||
const msg = messages.find(m => m.ruleId === 't/collector');
|
||||
if (!msg) return [];
|
||||
return JSON.parse(msg.message);
|
||||
}
|
||||
|
||||
describe('isWindowsExcludedNode — if (!windows) { assert } shape', () => {
|
||||
test('assert inside if (process.platform !== "win32") block → excluded=true', () => {
|
||||
const code = `
|
||||
if (process.platform !== 'win32') {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('assert inside else of if (process.platform === "win32") block → excluded=true', () => {
|
||||
const code = `
|
||||
if (process.platform === 'win32') {
|
||||
doWindows();
|
||||
} else {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('assert NOT inside any guard → excluded=false', () => {
|
||||
const code = `assert.equal(path.join(a, b), '/x/y');`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWindowsExcludedNode — early-return guard shape', () => {
|
||||
test('if (process.platform === "win32") return; assert.equal(...) → excluded=true', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
if (process.platform === 'win32') return;
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('if (process.platform === "win32") return t.skip(); assert.equal → excluded=true', () => {
|
||||
const code = `
|
||||
function test(t) {
|
||||
if (process.platform === 'win32') return t.skip('no windows');
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('guard appears AFTER the assert → excluded=false', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
if (process.platform === 'win32') return;
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWindowsExcludedNode — hoisted isWindows guard shape', () => {
|
||||
test('const isWindows = process.platform === "win32"; if (!isWindows) assert.equal → excluded=true', () => {
|
||||
const code = `
|
||||
const isWindows = process.platform === 'win32';
|
||||
if (!isWindows) assert.equal(path.join(a, b), '/x/y');
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('const isWindows = …; if (isWindows) {} else { assert.equal } → excluded=true', () => {
|
||||
const code = `
|
||||
const isWindows = process.platform === 'win32';
|
||||
if (isWindows) { doWindowsThing(); } else { assert.equal(path.join(a,b), '/x/y'); }
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWindowsExcludedNode — arbitrary-named hoisted boolean (real hoisting)', () => {
|
||||
test('const winFlag = process.platform === "win32"; if (!winFlag) assert.equal → excluded=true', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
const winFlag = process.platform === 'win32';
|
||||
if (!winFlag) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('const winFlag = process.platform === "win32"; if (winFlag) return; assert.equal → excluded=true', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
const winFlag = process.platform === 'win32';
|
||||
if (winFlag) return;
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('unrelated variable used as guard is NOT excluded', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
const debugMode = true;
|
||||
if (!debugMode) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false]);
|
||||
});
|
||||
|
||||
test('winFlag declared AFTER the assert is NOT a guard → excluded=false', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
const winFlag = process.platform === 'win32';
|
||||
if (!winFlag) { doSomething(); }
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── C2: early-return guard must climb ancestor blocks ────────────────────────
|
||||
|
||||
describe('C2 — early-return guard climbs ancestor blocks', () => {
|
||||
test('C2: early-return in outer block before nested if-block → excluded=true', () => {
|
||||
// The guard is in the function body; assert.equal is inside a nested if-block.
|
||||
// Before the fix, _hasEarlyWindowsReturnBefore only checked the innermost block.
|
||||
const code = `
|
||||
function test() {
|
||||
if (process.platform === 'win32') return;
|
||||
if (cond) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true]);
|
||||
});
|
||||
|
||||
test('C2: no early-return at all → excluded=false', () => {
|
||||
const code = `
|
||||
function test() {
|
||||
if (cond) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── C3: binding-aware identifier classification ──────────────────────────────
|
||||
|
||||
describe('C3 — binding-aware identifier classification', () => {
|
||||
test('C3 case 1: const isWindows = false; if (!isWindows) assert.equal → excluded=false (FLAGGED)', () => {
|
||||
// isWindows is in WINDOWS_BOOL_NAMES but its binding is `false`, not a platform test.
|
||||
const code = `
|
||||
function test() {
|
||||
const isWindows = false;
|
||||
if (!isWindows) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false], 'const isWindows = false should not be treated as a platform guard');
|
||||
});
|
||||
|
||||
test('C3 case 2: let w = platform===win32; w = false; if (!w) assert.equal → excluded=false (FLAGGED)', () => {
|
||||
// w starts as a platform test but is reassigned — should NOT be trusted.
|
||||
const code = `
|
||||
function test() {
|
||||
let w = process.platform === 'win32';
|
||||
w = false;
|
||||
if (!w) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false], 'reassigned variable should not be treated as a platform guard');
|
||||
});
|
||||
|
||||
test('C3 case 3: inner shadow const w = false wins over outer const w = platform test → excluded=false (FLAGGED)', () => {
|
||||
// The inner const w = false shadows the outer const w = platform test.
|
||||
const code = `
|
||||
function test() {
|
||||
const w = process.platform === 'win32';
|
||||
{
|
||||
const w = false;
|
||||
if (!w) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [false], 'inner shadow (w=false) should win over outer platform test');
|
||||
});
|
||||
|
||||
test('C3 case 4: const w = platform test; if (!w) assert.equal → excluded=true (genuine guard)', () => {
|
||||
// The canonical case — should still work.
|
||||
const code = `
|
||||
function test() {
|
||||
const w = process.platform === 'win32';
|
||||
if (!w) {
|
||||
assert.equal(path.join(a, b), '/x/y');
|
||||
}
|
||||
}
|
||||
`;
|
||||
const results = runCollector(code);
|
||||
assert.deepStrictEqual(results, [true], 'genuine platform guard should suppress the report');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user