Repository: sindresorhus/eslint-plugin-unicorn (JavaScript)

Issue:
Rule proposal: `no-empty-file`
There seems to be all kinds of "empty" rules for [block statements](https://eslint.org/docs/rules/no-empty), [destructuring](https://eslint.org/docs/rules/no-empty-pattern), [functions](https://eslint.org/docs/rules/no-empty-function), lines between things, etc., but interestingly nothing for entirely empty files, unless I am completely overlooking something (please correct me if I am wrong and I will humbly 🤦‍♂️).

I am more than happy to submit a PR if approved.

## Description

> Disallow empty files.

Disallow files containing absolutely nothing, only whitespace, or only comments.

## Fail

### No space or all whitespace

```js

```

### Only Return characters / new lines (or combination thereof) 

```js
\n
```

```js
\r
```

```js
\r\n
```

### Only comments

```js
// comment
```

```js
/* comment */
```

## Pass

<!-- 3️⃣ Specify an example of code that would be accepted in its place -->

```js
const value = 'string';
```



Discussion:
These should also consider "empty"

Only directive

```js
'use strict';
```

Only `EmptyStatement`s

```js
;;
```

Yes, "empty" meaning no actual code.
Good callouts @fisker! I am on board with that. 
I saw you already have an implementation for this rule, have you figured why it can't work for json files?
@fisker haha, ya caught me! 😊 Nice job on the sleuthing into my own repos! 🕵️‍♂️ I figured I'd take a stab at suggesting / moving some rules here for broader use and more resiliency. Unicorn, as a project, is structurally setup to be _*far*_ more "battle tested" with the way it generates eslint selectors, unit tests, integration tests, etc. Plus, you all think of things (like you did above) that I would never think of. I love the great minds and the community here and figured it was better to share / contribute. I hope that's ok. I would obviously deprecate my own rule and point it here, if accepted and merged.

In short, no. I haven't figured out why. One wouldn't typically be linting JSON files with Eslint, unless you're using [eslint-plugin-json](https://github.com/azeemba/eslint-plugin-json), which [it's own readme FAQ](https://github.com/azeemba/eslint-plugin-json#faqs) calls out it's own oddity with sections like [Why doesn't this plugin use `eslint` itself or just `JSON.parse`?](https://github.com/azeemba/eslint-plugin-json#why-doesnt-this-plugin-use-eslint-itself-or-just-jsonparse) and [Is `eslint` really the best tool to lint my JSON?](https://github.com/azeemba/eslint-plugin-json#is-eslint-really-the-best-tool-to-lint-my-json).

Using a `console.log` on the `Program` node, eslint seems to be unable to distinguish the difference between a "full" or empty JSON file, which is why I excluded `.json` file extensions in the first place. 

Logged output is below.

```
❯ yarn lint

yarn run v1.22.11

$ eslint . --ext .js --ext .json

empty-js-file.js Node {
  type: 'Program',
  start: 0,
  end: 0,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 0 },
    filename: undefined,
    identifierName: undefined
  },
  range: [ 0, 0 ],
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  comments: [],
  tokens: [],
  sourceType: 'module',
  body: [],
  parent: null
}

empty-json-file.json Node {
  type: 'Program',
  start: 0,
  end: 0,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 0 },
    filename: undefined,
    identifierName: undefined
  },
  range: [ 0, 0 ],
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  comments: [],
  tokens: [],
  sourceType: 'module',
  body: [],
  parent: null
}

not-empty-json-file.json Node {
  type: 'Program',
  start: 0,
  end: 0,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 0 },
    filename: undefined,
    identifierName: undefined
  },
  range: [ 0, 0 ],
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  comments: [],
  tokens: [],
  sourceType: 'module',
  body: [],
  parent: null
}

package.json Node {
  type: 'Program',
  start: 0,
  end: 0,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 0 },
    filename: undefined,
    identifierName: undefined
  },
  range: [ 0, 0 ],
  leadingComments: undefined,
  trailingComments: undefined,
  innerComments: undefined,
  extra: undefined,
  comments: [],
  tokens: [],
  sourceType: 'module',
  body: [],
  parent: null
}

empty-js-file.js
  1:1  error  Disallow empty files  no-empty-files

empty-json-file.json
  1:2  error  Disallow empty files  no-empty-files

not-empty-json-file.json
  1:2  error  Disallow empty files  no-empty-files

package.json
  1:2  error  Disallow empty files  no-empty-files
```

If it were to be accepted into unicorn, I could exclude the `.json` check or we could keep it if you wanted to remain compatible with eslint-plugin-json. Or you might have a better idea that I have not thought of yet. 

I haven't dug into the eslint-plugin-json codebase or opened an issue there either. Perhaps there's a deeper root cause to uncover.
@fisker I've dug into this a bit more... 

[eslint-plugin-json](https://github.com/azeemba/eslint-plugin-json) has issues because it's using a non-standard preprocessor (it's using [vscode-json-languageservice](https://www.npmjs.com/package/vscode-json-languageservice)) and currently returns an empty string, which makes it difficult for other plugins to work alongside of it (see https://github.com/azeemba/eslint-plugin-json/issues/38 for more information).

There appear to be more compatible ESLint JSON plugins, namely [eslint-plugin-json-es](https://github.com/zeitport/eslint-plugin-json-es) and [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc), which both use the standard ESLint parser [espree](https://github.com/eslint/espree) to create compliant AST. I've tested both plugins and neither one create the same issues as eslint-plugin-json.

Based on all of the above, my recommendation is to:

1. Not have a `.json` extension check within the rule.
2. Recommend more compatible ESLint JSON plugins if people have issues, which could also be called out on the rule docs, if desired.
Thanks for the information.

> Not have a .json extension check within the rule.

Agree. Users can also ignore files with [`overrides`](https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-based-on-glob-patterns)


This is now accepted.
I'm not sure how ESLint work for Vue SFC, please make sure to add test for a Vue SFC file without `<script>` block.
@fisker awesome! I'll get started on an implementation. Good note on the Vue SFC too. I'll add it to the list.
@fisker one last question... Name? `no-empty-file` or `no-empty-files`?
ESLint use `no-empty-pattern` and `no-empty-function`, so `no-empty-file`?
@fisker I think I agree. Everything else seems to be singular in unicorn too. Things aren't named `arrays`, `ternaries`, etc. Singular it is.



Patch:
diff --git a/configs/recommended.js b/configs/recommended.js
index 47ff8200be..868fd3ca79 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 38a8e72eda..ed9954d5f3 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",
@@ -179,6 +180,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,
+	},
+};


Test patch:
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