Add no=extra-boolean-cast test
MODIFY
packages/@romejs/js-compiler/__rtests__/lint.ts
packages/@romejs/js-compiler/__rtests__/lint.ts
@@ -69,3 +69,648 @@ test('format enabled in project config should result in regenerated file', async
);
t.is(res.src, "foobar('yes');\n");
});
+
+test('disallows comparing negative zero', async t => {
+ const sourceTextA = '(1 >= -0)';
+
+ const sourceTextB = '(1 >= 0)';
+
+ const res1 = await testLint(sourceTextA, LINT_AND_FORMAT_ENABLED_CONFIG);
+ t.looksLike(res1.diagnostics, [
+ {
+ category: 'lint/noCompareNegZero',
+ filename: 'unknown',
+ language: 'js',
+ message: "Do not use the '>=' operator to compare against -0.",
+ mtime: undefined,
+ sourceType: 'module',
+ origins: [{category: 'lint'}],
+ end: {
+ column: 8,
+ index: 8,
+ line: 1,
+ },
+ start: {
+ column: 1,
+ index: 1,
+ line: 1,
+ },
+ },
+ ]);
+
+ const res2 = await testLint(sourceTextB, LINT_AND_FORMAT_ENABLED_CONFIG);
+ t.looksLike(res2.diagnostics, []);
+});
+
+test('no cond assign', async t => {
+ const forLoop = await testLint(
+ `for (let i = 1; i = 10; i++) {
+ console.log('foo')
+ }`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.snapshot(forLoop);
+
+ const ifCondition = await testLint(
+ `if(foo = 'bar') {
+ console.log('foo')
+ }`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.snapshot(ifCondition);
+
+ const whileLoop = await testLint(
+ `while (foo = 'bar' {
+ console.log('foo')
+ }`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.snapshot(whileLoop);
+
+ const DoWhileLoop = await testLint(
+ `do {
+ console.log('foo)
+ } while (foo = 'bar')`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.snapshot(DoWhileLoop);
+});
+
+test('no label var', async t => {
+ const badLabel = await testLint(
+ `
+ const x = "test";
+ x: const y = "test";
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(badLabel.diagnostics.find(d => d.category === 'lint/noLabelVar'));
+
+ const okLabel = await testLint(
+ `
+ const x = "test";
+ z: const y = "test";
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.falsy(okLabel.diagnostics.find(d => d.category === 'lint/noLabelVar'));
+});
+
+test('no duplicate keys', async t => {
+ const res = await testLint(
+ `
+ const foo = {
+ test: true,
+ test2: true,
+ test: false,
+ }
+
+ // mark const as used
+ console.log(foo);
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.looksLike(res.diagnostics, [
+ {
+ category: 'lint/noDuplicateKeys',
+ filename: 'unknown',
+ language: 'js',
+ message: 'Duplicate key <emphasis>test</emphasis>',
+ mtime: undefined,
+ sourceType: 'module',
+ origins: [{category: 'lint'}],
+ end: {
+ column: 17,
+ index: 73,
+ line: 5,
+ },
+ start: {
+ column: 6,
+ index: 62,
+ line: 5,
+ },
+ },
+ ]);
+});
+
+test('no function reassignment', async t => {
+ function checkCategory(diagnostic: PartialDiagnostic): Boolean {
+ return diagnostic.category === 'lint/noFunctionAssign';
+ }
+
+ const validTestCases = [
+ 'function foo() { var foo = bar; }',
+ 'function foo(foo) { foo = bar; }',
+ 'function foo() { var foo; foo = bar; }',
+ 'var foo = () => {}; foo = bar;',
+ 'var foo = function() {}; foo = bar;',
+ 'var foo = function() { foo = bar; };',
+ `import bar from 'bar'; function foo() { var foo = bar; }`,
+ ];
+
+ const invalidTestCases = [
+ 'function foo() {}; foo = bar;',
+ 'function foo() { foo = bar; }',
+ 'foo = bar; function foo() { };',
+ '[foo] = bar; function foo() { };',
+ '({x: foo = 0} = bar); function foo() { };',
+ 'function foo() { [foo] = bar; }',
+ '(function() { ({x: foo = 0} = bar); function foo() { }; })();',
+ ];
+
+ for (const testCase of validTestCases) {
+ const {diagnostics} = await testLint(
+ testCase,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.falsy(diagnostics.find(checkCategory));
+ }
+
+ for (const testCase of invalidTestCases) {
+ const {diagnostics} = await testLint(
+ testCase,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.truthy(diagnostics.find(checkCategory));
+ }
+});
+
+test('no duplicated args allowed', async t => {
+ const duplicatedArgs = await testLint(
+ `
+ function hello(a, a) {
+ console.log("Hello);
+ }
+ hello();
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ duplicatedArgs.diagnostics.find(d => d.category === 'lint/noDupeArgs'),
+ );
+});
+
+test('disallow var', async t => {
+ const res = await testLint(
+ 'var foobar;\nfoobar',
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.snapshot(res);
+
+ // Redundant because of the snapshot above, but this is what we actually care about
+ t.looksLike(res.diagnostics, [
+ {
+ category: 'lint/disallowVar',
+ filename: 'unknown',
+ language: 'js',
+ message:
+ 'Variable declarations using `var` are disallowed, use `let` or `const` instead.',
+ mtime: undefined,
+ sourceType: 'module',
+ origins: [{category: 'lint'}],
+ end: {
+ column: 11,
+ index: 11,
+ line: 1,
+ },
+ start: {
+ column: 0,
+ index: 0,
+ line: 1,
+ },
+ },
+ ]);
+});
+
+test('no empty character class in regular expression', async t => {
+ const validTestCases = [
+ 'let foo = /^abc[a-zA-Z]/;foo;',
+ 'let regExp = new RegExp("^abc[]");regExp;',
+ 'let foo = /^abc/;foo;',
+ 'let foo = /[\\[]/;foo;',
+ 'let foo = /[\\]]/;foo;',
+ 'let foo = /[a-zA-Z\\[]/;foo;',
+ 'let foo = /[[]/;foo;',
+ 'let foo = /[\\[a-z[]]/;foo;',
+ 'let foo = /[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\^\\$\\|]/g;foo;',
+ 'let foo = /[\\]]/uy;foo;',
+ 'let foo = /[\\]]/s;foo;',
+ 'let foo = /\\[]/;foo;',
+ ];
+
+ const invalidTestCases = [
+ 'let foo = /^abc[]/;foo;',
+ 'let foo = /foo[]bar/;foo;',
+ 'let foo = "";if (foo.match(/^abc[]/)) { foo; }',
+ 'let foo = /[]]/;foo;',
+ 'let foo = /\\[[]/;foo;',
+ 'let foo = /\\[\\[\\]a-z[]/;foo;',
+ ];
+
+ for (const validTestCase of validTestCases) {
+ const {diagnostics} = await testLint(
+ validTestCase,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.is(diagnostics.length, 0);
+ }
+
+ for (const invalidTestCase of invalidTestCases) {
+ let res = await testLint(
+ invalidTestCase,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.snapshot(res);
+ }
+});
+
+test('disallow unsafe usage of break, continue, throw and return', async t => {
+ const returnTest = await testLint(
+ `
+ function greet1() {
+ try {
+ throw new Error("Try")
+ } catch(err) {
+ throw err;
+ } finally {
+ return 1;
+ }
+ }
+
+ greet1();
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ returnTest.diagnostics.find(
+ d => d.message === `Unsafe usage of ReturnStatement.`,
+ ),
+ );
+
+ const breakTest = await testLint(
+ `
+
+ function greet2() {
+ try {
+ throw new Error("Try")
+ } catch(err) {
+ throw err;
+ } finally {
+ break;
+ }
+ }
+
+ greet2();
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ breakTest.diagnostics.find(
+ d => d.message === `Unsafe usage of BreakStatement.`,
+ ),
+ );
+
+ const continueTest = await testLint(
+ `
+ function greet3() {
+ try {
+ throw new Error("Try")
+ } catch(err) {
+ throw err;
+ } finally {
+ continue;
+ }
+ }
+
+ greet3();
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ continueTest.diagnostics.find(
+ d => d.message === `Unsafe usage of ContinueStatement.`,
+ ),
+ );
+
+ const throwTest = await testLint(
+ `
+ function greet4() {
+ try {
+ throw new Error("Try")
+ } catch(err) {
+ throw err;
+ } finally {
+ throw new Error("Finally");
+ }
+ }
+
+ greet4();
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ throwTest.diagnostics.find(
+ d => d.message === `Unsafe usage of ThrowStatement.`,
+ ),
+ );
+});
+
+test('no delete vars', async t => {
+ const res = await testLint(
+ `
+ const foo = "test";
+ delete foo;
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ 'script',
+ );
+
+ t.looksLike(res.diagnostics, [
+ {
+ category: 'lint/noDeleteVars',
+ message: 'Variables should not be deleted.',
+ mtime: undefined,
+ filename: 'unknown',
+ start: {index: 29, line: 3, column: 4},
+ end: {index: 39, line: 3, column: 14},
+ language: 'js',
+ sourceType: 'script',
+ origins: [{category: 'lint'}],
+ },
+ ]);
+});
+
+test('no template curly in string', async t => {
+ const res = await testLint(
+ `
+ const user = "Faustina";
+ const helloUser = "Hello, \${user}!";
+
+ // mark consts as used
+ console.log(user, helloUser)
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.looksLike(res.diagnostics, [
+ {
+ category: 'lint/noTemplateCurlyInString',
+ filename: 'unknown',
+ language: 'js',
+ message: 'Unexpected template string expression.',
+ mtime: undefined,
+ sourceType: 'module',
+ origins: [{category: 'lint'}],
+ end: {column: 39, index: 69, line: 3},
+ start: {column: 22, index: 52, line: 3},
+ },
+ ]);
+});
+
+test('no import assign', async t => {
+ let failingCases = [
+ 'import x from "y";\nx=1;',
+ 'import x from "y";\n[x]=1;',
+ 'import x from "y";\n({x}=1);',
+ 'import x from "y";\nx++',
+ 'import x from "y";\n[...x]=1;',
+ 'import x from "y";\n({...x}=1);',
+ 'import x from "y";\nfor (x in y);',
+ 'import x from "y";\nx+=1',
+ 'import * as x from "y";\nx=1;',
+ 'import {x} from "y";\nx=1;',
+ ];
+ for (let failingCase of failingCases) {
+ const res = await testLint(
+ failingCase,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ if (!res.diagnostics.some(d => d.category === 'lint/noImportAssign')) {
+ t.fail(
+ `expected "\n${failingCase}\n" to report a lint/noImportAssign diagnostic but it didn't`,
+ [
+ {
+ type: 'inspect',
+ data: parseJS({
+ input: failingCase,
+ sourceType: 'module',
+ path: createUnknownFilePath('unknown'),
+ }),
+ },
+ {type: 'inspect', data: res.diagnostics},
+ ],
+ );
+ }
+ }
+});
+
+test('no debugger', async t => {
+ const goodRes = await testLint(
+ `const test = { debugger: 1 };
+ test.debugger;
+ console.log(test); // To not trigger the unused var rule.
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.is(goodRes.diagnostics.length, 0);
+
+ const badRes = await testLint(
+ 'debugger;',
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.snapshot(badRes.diagnostics);
+});
+
+test('disallow multiple spaces in regular expression literals', async t => {
+ const res1 = await testLint(
+ `new RegExp("foo bar")`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.looksLike(res1.diagnostics, [
+ {
+ category: 'lint/disallowMultipleSpacesInRegularExpressionLiterals',
+ message: 'Disallow multiple spaces in regular expression literals',
+ mtime: undefined,
+ filename: 'unknown',
+ start: {index: 0, line: 1, column: 0},
+ end: {index: 22, line: 1, column: 22},
+ language: 'js',
+ sourceType: 'module',
+ origins: [{category: 'lint'}],
+ },
+ ]);
+
+ const res2 = await testLint(
+ `new RegExp("foo {2}bar")`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.looksLike(res2.diagnostics, []);
+
+ const res3 = await testLint(
+ `/foo bar/`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ t.looksLike(res3.diagnostics, [
+ {
+ category: 'lint/disallowMultipleSpacesInRegularExpressionLiterals',
+ message: 'Disallow multiple spaces in regular expression literals',
+ mtime: undefined,
+ filename: 'unknown',
+ start: {index: 0, line: 1, column: 0},
+ end: {index: 10, line: 1, column: 10},
+ language: 'js',
+ sourceType: 'module',
+ origins: [{category: 'lint'}],
+ },
+ ]);
+
+ const res4 = await testLint(
+ `/foo {2}bar/`,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.looksLike(res4.diagnostics, []);
+});
+
+test('disallow unnecessary boolean casts', async t => {
+ const ifTest = await testLint(
+ `
+ if (Boolean(foo)) {
+ return foo;
+ }
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ ifTest.diagnostics.find(d => d.message === `Redundant double negation.`),
+ );
+
+ const whileTest = await testLint(
+ `
+ while (!!foo) {
+ return foo;
+ }
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ whileTest.diagnostics.find(d => d.message === `Redundant double negation.`),
+ );
+
+ const doWhileTest = await testLint(
+ `
+ let x = 1;
+
+ do {
+ 1+1;
+ } while (Boolean(x));
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ doWhileTest.diagnostics.find(
+ d => d.message === `Redundant double negation.`,
+ ),
+ );
+
+ const forTest = await testLint(
+ `
+ for (; !!foo; ) {
+ return 1+1;
+ }
+ `,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+
+ t.truthy(
+ forTest.diagnostics.find(d => d.message === `Redundant double negation.`),
+ );
+});
+test('no shadow restricted names', async t => {
+ let failingCases = [
+ 'function NaN() {}',
+ 'let Set;',
+ '!function Array() {}',
+ 'function test(JSON) {}',
+ 'try { } catch(Object) {}',
+ ];
+ for (let failingCase of failingCases) {
+ const res = await testLint(
+ failingCase,
+ LINT_ENABLED_FORMAT_DISABLED_CONFIG,
+ );
+ if (
+ !res.diagnostics.some(d => d.category === 'lint/noShadowRestrictedNames')
+ ) {
+ t.fail(
+ `expected "\n${failingCase}\n" to report a lint/noShadowRestrictedNames diagnostic but it didn't`,
+ [
+ {
+ type: 'inspect',
+ data: parseJS({
+ input: failingCase,
+ sourceType: 'module',
+ path: createUnknownFilePath('unknown'),
+ }),
+ },
+ {type: 'inspect', data: res.diagnostics},
+ ],
+ );
+ }
+ }
+});
+
+test('prefer function declarations', async t => {
+ // Should complain on these
+ t.snapshot(
+ await testLint(
+ 'const foo = function () {};',
+ LINT_AND_FORMAT_ENABLED_CONFIG,
+ ),
+ );
+ t.snapshot(
+ await testLint('const foo = () => {};', LINT_AND_FORMAT_ENABLED_CONFIG),
+ );
+
+ // Should allow arrow functions when they have this
+ t.snapshot(
+ await testLint(
+ 'const foo = () => {this;};',
+ LINT_AND_FORMAT_ENABLED_CONFIG,
+ ),
+ );
+
+ // But only if it refers to the actual arrow function
+ t.snapshot(
+ await testLint(
+ 'const foo = () => {function bar() {this;}};',
+ LINT_AND_FORMAT_ENABLED_CONFIG,
+ ),
+ );
+
+ // Should ignore functions with return types since you can't express that with a declaration
+ t.snapshot(
+ await testLint(
+ 'const foo: any = function () {};',
+ LINT_AND_FORMAT_ENABLED_CONFIG,
+ ),
+ );
+});

Add no=extra-boolean-cast test