339 lines
11 KiB
JavaScript
339 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* no-crlf-fragile-split.rule.test.cjs
|
|
*
|
|
* RuleTester unit tests for the local/no-crlf-fragile-split ESLint rule.
|
|
*
|
|
* Rule covers three sub-patterns:
|
|
* G1 — .split('\n') on readFileSync-derived content (crlfFragileSplit)
|
|
* G2 — RegExp with bare \n on readFileSync-derived content (crlfFragileRegex)
|
|
* G3 — RegExp with bare \n containing markdown fence or frontmatter anchor
|
|
* (crlfFragileRegex) — caught even without direct data-flow
|
|
*
|
|
* NOTE: Fixture code strings must encode actual \n characters as \\n inside
|
|
* the JavaScript string literals used for RuleTester `code` fields, so that
|
|
* the ESLint parser receives the intended source text.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { RuleTester } = require('eslint');
|
|
|
|
const rule = require('../eslint-rules/no-crlf-fragile-split.cjs');
|
|
|
|
const ruleTester = new RuleTester({
|
|
languageOptions: {
|
|
ecmaVersion: 2022,
|
|
sourceType: 'commonjs',
|
|
},
|
|
});
|
|
|
|
// ─── module shape ─────────────────────────────────────────────────────────────
|
|
|
|
describe('no-crlf-fragile-split rule module', () => {
|
|
test('exports meta and create', () => {
|
|
assert.strictEqual(typeof rule.meta, 'object');
|
|
assert.strictEqual(typeof rule.create, 'function');
|
|
assert.strictEqual(rule.meta.type, 'problem');
|
|
assert.ok(rule.meta.messages.crlfFragileSplit, 'crlfFragileSplit message must exist');
|
|
assert.ok(rule.meta.messages.crlfFragileRegex, 'crlfFragileRegex message must exist');
|
|
});
|
|
});
|
|
|
|
// ─── G1: INVALID cases ────────────────────────────────────────────────────────
|
|
|
|
describe('G1 — no-crlf-fragile-split: invalid (crlfFragileSplit)', () => {
|
|
test('G1-invalid: readFileSync(p).split("\\n") — direct chain', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// code that ESLint will parse: fs.readFileSync(p, 'utf8').split('\n')
|
|
code: "const lines = fs.readFileSync(p, 'utf8').split('\\n');",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileSplit' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G1-invalid: readFileSync(p).toString().split("\\n") — chained call', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: "const lines = readFileSync(p).toString().split('\\n');",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileSplit' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G1-invalid: content = readFileSync(...); content.split("\\n") — via variable', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: [
|
|
"const content = fs.readFileSync(filePath, 'utf8');",
|
|
"const lines = content.split('\\n');",
|
|
].join('\n'),
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileSplit' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G1-invalid: double-quoted "\\n" in split', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: 'const lines = fs.readFileSync(\'file.txt\', \'utf8\').split("\\n");',
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileSplit' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── G1: VALID cases ──────────────────────────────────────────────────────────
|
|
|
|
describe('G1 — no-crlf-fragile-split: valid cases', () => {
|
|
test('G1-valid: .split(/\\r?\\n/) — correct regex', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
// /\r?\n/ in source — no bare \n in a string argument
|
|
code: "const lines = fs.readFileSync(p, 'utf8').split(/\\r?\\n/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('G1-valid: non-file content split — "\\n" on a plain string literal', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: "const lines = someString.split('\\n');",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('G1-valid: non-file content split — "\\n" on variable not from readFileSync', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: [
|
|
"const content = 'hello\\\\nworld';",
|
|
"const lines = content.split('\\n');",
|
|
].join('\n'),
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('G1-valid: .split("\\n") on a fetch/HTTP response (not readFileSync)', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: "const lines = response.text.split('\\n');",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── G2/G3: INVALID cases ─────────────────────────────────────────────────────
|
|
|
|
describe('G2/G3 — no-crlf-fragile-split: invalid (crlfFragileRegex)', () => {
|
|
test('G2-invalid: bare \\n in regex on readFileSync content via .match()', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// /foo\nbar/ — regex with bare \n; .match() on readFileSync result
|
|
code: "const m = fs.readFileSync(p, 'utf8').match(/foo\\nbar/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G2-invalid: bare \\n in regex on readFileSync content via .test()', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// /hello\nworld/.test(readFileSync(...))
|
|
code: "const ok = /hello\\nworld/.test(fs.readFileSync(p, 'utf8'));",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G2-invalid: bare \\n in regex on readFileSync content via .replace()', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
code: "const out = fs.readFileSync(p, 'utf8').replace(/foo\\nbar/, 'x');",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G3-invalid: markdown fence regex with bare \\n (```bash\\n)', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// content.match(/```bash\nsome/) — fence regex with bare \n
|
|
code: "const m = content.match(/```bash\\nsome/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('G3-invalid: frontmatter anchor regex with bare \\n (/^---\\n/)', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// /^---\ntitle/.test(content) — frontmatter with bare \n
|
|
code: "const hasFM = /^---\\ntitle/.test(content);",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── G2/G3: VALID cases ───────────────────────────────────────────────────────
|
|
|
|
describe('G2/G3 — no-crlf-fragile-split: valid cases', () => {
|
|
test('G2-valid: regex with \\r?\\n (already CRLF-safe) on readFileSync content', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
// /foo\r?\nbar/ — has \r?\n so it's safe
|
|
code: "const m = fs.readFileSync(p, 'utf8').match(/foo\\r?\\nbar/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('G2-valid: regex with bare \\n but used on a non-file string (ok per known boundaries)', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
// /hello\nworld/.test(someRuntimeString) — not file content
|
|
code: "const ok = /hello\\nworld/.test(someRuntimeString);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('G3-valid: markdown fence regex but with \\r?\\n (already CRLF-safe)', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: "const m = content.match(/```bash\\r?\\nsome/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('G3-valid: frontmatter regex with \\r\\n (explicitly CRLF-safe)', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: "const hasFM = /^---\\r\\ntitle/.test(content);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
// C3 per-occurrence classification cases
|
|
test('C3-valid: /\\r?\\n/ — single safe occurrence, not flagged', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: "const m = fs.readFileSync(p, 'utf8').match(/\\r?\\n/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
|
|
test('C3-invalid: regex with both safe \\r?\\n AND a separate bare \\n — flagged', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// /\r?\nfoo|\nbar/ — the second \n (after |) is bare and fragile
|
|
code: "const m = fs.readFileSync(p, 'utf8').match(/\\r?\\nfoo|\\nbar/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('C3-invalid: [^\\n] — \\n in class without \\r is fragile', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [],
|
|
invalid: [
|
|
{
|
|
// /[^\n]+/ — class has \n but no \r
|
|
code: "const m = fs.readFileSync(p, 'utf8').match(/[^\\n]+/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
errors: [{ messageId: 'crlfFragileRegex' }],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('C3-valid: [^\\r\\n] — \\n in class with \\r is safe', () => {
|
|
ruleTester.run('no-crlf-fragile-split', rule, {
|
|
valid: [
|
|
{
|
|
code: "const m = fs.readFileSync(p, 'utf8').match(/[^\\r\\n]+/);",
|
|
filename: 'tests/foo.test.cjs',
|
|
},
|
|
],
|
|
invalid: [],
|
|
});
|
|
});
|
|
});
|