diff --git a/configs/recommended.js b/configs/recommended.js index 3985e9232a..0b9bd2e48c 100644 --- a/configs/recommended.js +++ b/configs/recommended.js @@ -24,6 +24,7 @@ module.exports = { 'unicorn/no-array-reduce': 'error', 'unicorn/no-console-spaces': 'error', 'unicorn/no-document-cookie': 'error', + 'unicorn/no-empty-file': 'error', 'unicorn/no-for-loop': 'error', 'unicorn/no-hex-escape': 'error', 'unicorn/no-instanceof-array': 'error', diff --git a/docs/rules/no-empty-file.md b/docs/rules/no-empty-file.md new file mode 100644 index 0000000000..5d391eecd4 --- /dev/null +++ b/docs/rules/no-empty-file.md @@ -0,0 +1,65 @@ +# Disallow empty files + +Meaningless files clutter a codebase. + +Disallow any files only containing the following: + +- Whitespace +- Comments +- Directives +- Empty statements +- Empty block statements +- Hashbang + +## Fail + +```js + +``` + +```js +// Comment +``` + +```js +/* Comment */ +``` + +```js +'use strict'; +``` + +```js +; +``` + +```js +{ +} +``` + +```js +#!/usr/bin/env node +``` + +## Pass + +```js +const x = 0; +``` + +```js +'use strict'; +const x = 0; +``` + +```js +;; +const x = 0; +``` + +```js +{ + const x = 0; +} +``` diff --git a/readme.md b/readme.md index 9ab5ce97e5..a82874ccf0 100644 --- a/readme.md +++ b/readme.md @@ -57,6 +57,7 @@ Configure it in `package.json`. "unicorn/no-array-reduce": "error", "unicorn/no-console-spaces": "error", "unicorn/no-document-cookie": "error", + "unicorn/no-empty-file": "error", "unicorn/no-for-loop": "error", "unicorn/no-hex-escape": "error", "unicorn/no-instanceof-array": "error", @@ -180,6 +181,7 @@ Each rule has emojis denoting: | [no-array-reduce](docs/rules/no-array-reduce.md) | Disallow `Array#reduce()` and `Array#reduceRight()`. | βœ… | | | | [no-console-spaces](docs/rules/no-console-spaces.md) | Do not use leading/trailing space between `console.log` parameters. | βœ… | πŸ”§ | | | [no-document-cookie](docs/rules/no-document-cookie.md) | Do not use `document.cookie` directly. | βœ… | | | +| [no-empty-file](docs/rules/no-empty-file.md) | Disallow empty files. | βœ… | | | | [no-for-loop](docs/rules/no-for-loop.md) | Do not use a `for` loop that can be replaced with a `for-of` loop. | βœ… | πŸ”§ | | | [no-hex-escape](docs/rules/no-hex-escape.md) | Enforce the use of Unicode escapes instead of hexadecimal escapes. | βœ… | πŸ”§ | | | [no-instanceof-array](docs/rules/no-instanceof-array.md) | Require `Array.isArray()` instead of `instanceof Array`. | βœ… | πŸ”§ | | diff --git a/rules/no-empty-file.js b/rules/no-empty-file.js new file mode 100644 index 0000000000..db24d82f21 --- /dev/null +++ b/rules/no-empty-file.js @@ -0,0 +1,47 @@ +'use strict'; + +const MESSAGE_ID = 'no-empty-file'; +const messages = { + [MESSAGE_ID]: 'Empty files are not allowed.', +}; + +const isEmpty = node => + ( + (node.type === 'Program' || node.type === 'BlockStatement') + && node.body.every(currentNode => isEmpty(currentNode)) + ) + || node.type === 'EmptyStatement' + || (node.type === 'ExpressionStatement' && 'directive' in node); + +const create = context => { + const filename = context.getPhysicalFilename().toLowerCase(); + + if (!/\.(?:js|mjs|cjs|ts|mts|cts)$/.test(filename)) { + return {}; + } + + return { + Program(node) { + if (!isEmpty(node)) { + return; + } + + return { + node, + messageId: MESSAGE_ID, + }; + }, + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + description: 'Disallow empty files.', + }, + schema: [], + messages, + }, +}; diff --git a/test/no-empty-file.mjs b/test/no-empty-file.mjs new file mode 100644 index 0000000000..1669056099 --- /dev/null +++ b/test/no-empty-file.mjs @@ -0,0 +1,74 @@ +import outdent from 'outdent'; +import {getTester} from './utils/test.mjs'; + +const {test} = getTester(import.meta); + +test.snapshot({ + valid: [ + ...[ + 'const x = 0;', + ';; const x = 0;', + '{{{;;const x = 0;}}}', + outdent` + 'use strict'; + const x = 0; + `, + ';;\'use strict\';', + '{\'use strict\';}', + '("use strict")', + '`use strict`', + '({})', + outdent` + #!/usr/bin/env node + console.log('done'); + `, + 'false', + '("")', + 'NaN', + 'undefined', + 'null', + '[]', + '(() => {})()', + ].map(code => ({code, filename: 'example.js'})), + '', + ...[ + 'md', + 'vue', + 'svelte', + 'tsx', + ].map(extension => ({code: '', filename: `example.${extension}`})), + ], + invalid: [ + ...[ + '', + '\uFEFF', + ' ', + '\t', + '\n', + '\r', + '\r\n', + outdent` + + `, + '// comment', + '/* comment */', + '#!/usr/bin/env node', + '\'use asm\';', + '\'use strict\';', + '"use strict"', + '""', + ';', + ';;', + '{}', + '{;;}', + '{{}}', + ].map(code => ({code, filename: 'example.js'})), + ...[ + 'mjs', + 'cjs', + 'ts', + 'mts', + 'cts', + ].map(extension => ({code: '{}', filename: `example.${extension}`})), + ], +}); diff --git a/test/snapshots/no-empty-file.mjs.md b/test/snapshots/no-empty-file.mjs.md new file mode 100644 index 0000000000..0bbc8f1f61 --- /dev/null +++ b/test/snapshots/no-empty-file.mjs.md @@ -0,0 +1,414 @@ +# Snapshot report for `test/no-empty-file.mjs` + +The actual snapshot is saved in `no-empty-file.mjs.snap`. + +Generated by [AVA](https://avajs.dev). + +## Invalid #1 + 1 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 |␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #2 + 1 | ο»Ώ + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | ␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #3 + 1 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | ␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #4 + 1 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | ␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #5 + 1 | + 2 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 |␊ + | ^␊ + > 2 |␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #6 + 1 | + 2 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 |␊ + | ^␊ + > 2 |␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #7 + 1 | + 2 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 |␊ + | ^␊ + > 2 |␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #8 + 1 | + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 |␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #9 + 1 | // comment + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | // comment␊ + | ^^^^^^^^^^ Empty files are not allowed.␊ + ` + +## Invalid #10 + 1 | /* comment */ + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | /* comment */␊ + | ^^^^^^^^^^^^^ Empty files are not allowed.␊ + ` + +## Invalid #11 + 1 | #!/usr/bin/env node + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | #!/usr/bin/env node␊ + | ^^^^^^^^^^^^^^^^^^^ Empty files are not allowed.␊ + ` + +## Invalid #12 + 1 | 'use asm'; + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | 'use asm';␊ + | ^^^^^^^^^^ Empty files are not allowed.␊ + ` + +## Invalid #13 + 1 | 'use strict'; + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | 'use strict';␊ + | ^^^^^^^^^^^^^ Empty files are not allowed.␊ + ` + +## Invalid #14 + 1 | "use strict" + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | "use strict"␊ + | ^^^^^^^^^^^^ Empty files are not allowed.␊ + ` + +## Invalid #15 + 1 | "" + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | ""␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #16 + 1 | ; + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | ;␊ + | ^ Empty files are not allowed.␊ + ` + +## Invalid #17 + 1 | ;; + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | ;;␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #18 + 1 | {} + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | {}␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #19 + 1 | {;;} + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | {;;}␊ + | ^^^^ Empty files are not allowed.␊ + ` + +## Invalid #20 + 1 | {{}} + +> Filename + + `␊ + example.js␊ + ` + +> Error 1/1 + + `␊ + > 1 | {{}}␊ + | ^^^^ Empty files are not allowed.␊ + ` + +## Invalid #21 + 1 | {} + +> Filename + + `␊ + example.mjs␊ + ` + +> Error 1/1 + + `␊ + > 1 | {}␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #22 + 1 | {} + +> Filename + + `␊ + example.cjs␊ + ` + +> Error 1/1 + + `␊ + > 1 | {}␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #23 + 1 | {} + +> Filename + + `␊ + example.ts␊ + ` + +> Error 1/1 + + `␊ + > 1 | {}␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #24 + 1 | {} + +> Filename + + `␊ + example.mts␊ + ` + +> Error 1/1 + + `␊ + > 1 | {}␊ + | ^^ Empty files are not allowed.␊ + ` + +## Invalid #25 + 1 | {} + +> Filename + + `␊ + example.cts␊ + ` + +> Error 1/1 + + `␊ + > 1 | {}␊ + | ^^ Empty files are not allowed.␊ + ` diff --git a/test/snapshots/no-empty-file.mjs.snap b/test/snapshots/no-empty-file.mjs.snap new file mode 100644 index 0000000000..f383ae6dae Binary files /dev/null and b/test/snapshots/no-empty-file.mjs.snap differ