Skip to content

Commit

Permalink
feat(eslint-plugin): [no-var-requires, no-require-imports] allow opti…
Browse files Browse the repository at this point in the history
…on (#7710)

* feat(eslint-plugin): [no-var-requires, no-require-imports] allowPackageJson option

* Use allow option

* Update snap

* Fix test
  • Loading branch information
Josh-Cena committed Jan 4, 2024
1 parent 1a8e0dc commit f7b5ca8
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 16 deletions.
22 changes: 22 additions & 0 deletions packages/eslint-plugin/docs/rules/no-require-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ import { lib2 } from 'lib2';
import * as lib3 from 'lib3';
```

## Options

### `allow`

A array of strings. These strings will be compiled into regular expressions with the `u` flag and be used to test against the imported path. A common use case is to allow importing `package.json`. This is because `package.json` commonly lives outside of the TS root directory, so statically importing it would lead to root directory conflicts, especially with `resolveJsonModule` enabled. You can also use it to allow importing any JSON if your environment doesn't support JSON modules, or use it for other cases where `import` statements cannot work.

With `{allow: ['/package\\.json$']}`:

<!--tabs-->

### ❌ Incorrect

```ts
console.log(require('../data.json').version);
```

### ✅ Correct

```ts
console.log(require('../package.json').version);
```

## When Not To Use It

If your project frequently uses older CommonJS `require`s, then this rule might not be applicable to you.
Expand Down
22 changes: 22 additions & 0 deletions packages/eslint-plugin/docs/rules/no-var-requires.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,28 @@ require('foo');
import foo from 'foo';
```

## Options

### `allow`

A array of strings. These strings will be compiled into regular expressions with the `u` flag and be used to test against the imported path. A common use case is to allow importing `package.json`. This is because `package.json` commonly lives outside of the TS root directory, so statically importing it would lead to root directory conflicts, especially with `resolveJsonModule` enabled. You can also use it to allow importing any JSON if your environment doesn't support JSON modules, or use it for other cases where `import` statements cannot work.

With `{allow: ['/package\\.json$']}`:

<!--tabs-->

### ❌ Incorrect

```ts
const foo = require('../data.json');
```

### ✅ Correct

```ts
const foo = require('../package.json');
```

## When Not To Use It

If your project frequently uses older CommonJS `require`s, then this rule might not be applicable to you.
Expand Down
51 changes: 45 additions & 6 deletions packages/eslint-plugin/src/rules/no-require-imports.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
import type { TSESTree } from '@typescript-eslint/utils';
import { ASTUtils } from '@typescript-eslint/utils';
import { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';
import { getScope } from '@typescript-eslint/utils/eslint-utils';

import { createRule } from '../util';
import * as util from '../util';

export default createRule({
type Options = [
{
allow: string[];
},
];
type MessageIds = 'noRequireImports';

export default util.createRule<Options, MessageIds>({
name: 'no-require-imports',
meta: {
type: 'problem',
docs: {
description: 'Disallow invocation of `require()`',
},
schema: [],
schema: [
{
type: 'object',
properties: {
allow: {
type: 'array',
items: { type: 'string' },
description: 'Patterns of import paths to allow requiring from.',
},
},
additionalProperties: false,
},
],
messages: {
noRequireImports: 'A `require()` style import is forbidden.',
},
},
defaultOptions: [],
create(context) {
defaultOptions: [{ allow: [] }],
create(context, options) {
const allowPatterns = options[0].allow.map(
pattern => new RegExp(pattern, 'u'),
);
function isImportPathAllowed(importPath: string): boolean {
return allowPatterns.some(pattern => importPath.match(pattern));
}
return {
'CallExpression[callee.name="require"]'(
node: TSESTree.CallExpression,
): void {
if (
node.arguments[0]?.type === AST_NODE_TYPES.Literal &&
typeof node.arguments[0].value === 'string' &&
isImportPathAllowed(node.arguments[0].value)
) {
return;
}
const variable = ASTUtils.findVariable(getScope(context), 'require');

// ignore non-global require usage as it's something user-land custom instead
Expand All @@ -34,6 +66,13 @@ export default createRule({
}
},
TSExternalModuleReference(node): void {
if (
node.expression.type === AST_NODE_TYPES.Literal &&
typeof node.expression.value === 'string' &&
isImportPathAllowed(node.expression.value)
) {
return;
}
context.report({
node,
messageId: 'noRequireImports',
Expand Down
37 changes: 33 additions & 4 deletions packages/eslint-plugin/src/rules/no-var-requires.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { getScope } from '@typescript-eslint/utils/eslint-utils';

import { createRule } from '../util';

type Options = [];
type Options = [
{
allow: string[];
},
];
type MessageIds = 'noVarReqs';

export default createRule<Options, MessageIds>({
Expand All @@ -18,14 +22,39 @@ export default createRule<Options, MessageIds>({
messages: {
noVarReqs: 'Require statement not part of import statement.',
},
schema: [],
schema: [
{
type: 'object',
properties: {
allow: {
type: 'array',
items: { type: 'string' },
description: 'Patterns of import paths to allow requiring from.',
},
},
additionalProperties: false,
},
],
},
defaultOptions: [],
create(context) {
defaultOptions: [{ allow: [] }],
create(context, options) {
const allowPatterns = options[0].allow.map(
pattern => new RegExp(pattern, 'u'),
);
function isImportPathAllowed(importPath: string): boolean {
return allowPatterns.some(pattern => importPath.match(pattern));
}
return {
'CallExpression[callee.name="require"]'(
node: TSESTree.CallExpression,
): void {
if (
node.arguments[0]?.type === AST_NODE_TYPES.Literal &&
typeof node.arguments[0].value === 'string' &&
isImportPathAllowed(node.arguments[0].value)
) {
return;
}
const parent =
node.parent.type === AST_NODE_TYPES.ChainExpression
? node.parent.parent
Expand Down
77 changes: 77 additions & 0 deletions packages/eslint-plugin/tests/rules/no-require-imports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ import { createRequire } from 'module';
const require = createRequire();
require('remark-preset-prettier');
`,
{
code: "const pkg = require('./package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "const pkg = require('../package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "const pkg = require('../packages/package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "import pkg = require('../packages/package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "import pkg = require('data.json');",
options: [{ allow: ['\\.json$'] }],
},
{
code: "import pkg = require('some-package');",
options: [{ allow: ['^some-package$'] }],
},
],
invalid: [
{
Expand Down Expand Up @@ -111,5 +135,58 @@ var lib5 = require?.('lib5'),
},
],
},
{
code: "const pkg = require('./package.json');",
errors: [
{
line: 1,
column: 13,
messageId: 'noRequireImports',
},
],
},
{
code: "const pkg = require('./package.jsonc');",
options: [{ allow: ['/package\\.json$'] }],
errors: [
{
line: 1,
column: 13,
messageId: 'noRequireImports',
},
],
},
{
code: "import pkg = require('./package.json');",
errors: [
{
line: 1,
column: 14,
messageId: 'noRequireImports',
},
],
},
{
code: "import pkg = require('./package.jsonc');",
options: [{ allow: ['/package\\.json$'] }],
errors: [
{
line: 1,
column: 14,
messageId: 'noRequireImports',
},
],
},
{
code: "import pkg = require('./package.json');",
options: [{ allow: ['^some-package$'] }],
errors: [
{
line: 1,
column: 14,
messageId: 'noRequireImports',
},
],
},
],
});
52 changes: 52 additions & 0 deletions packages/eslint-plugin/tests/rules/no-var-requires.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ import { createRequire } from 'module';
const require = createRequire('foo');
const json = require('./some.json');
`,
{
code: "const pkg = require('./package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "const pkg = require('../package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "const pkg = require('../packages/package.json');",
options: [{ allow: ['/package\\.json$'] }],
},
{
code: "const pkg = require('data.json');",
options: [{ allow: ['\\.json$'] }],
},
{
code: "const pkg = require('some-package');",
options: [{ allow: ['^some-package$'] }],
},
],
invalid: [
{
Expand Down Expand Up @@ -157,5 +177,37 @@ configValidator.addSchema(require('./a.json'));
},
],
},
{
code: "const pkg = require('./package.json');",
errors: [
{
line: 1,
column: 13,
messageId: 'noVarReqs',
},
],
},
{
code: "const pkg = require('./package.jsonc');",
options: [{ allow: ['/package\\.json$'] }],
errors: [
{
line: 1,
column: 13,
messageId: 'noVarReqs',
},
],
},
{
code: "const pkg = require('./package.json');",
options: [{ allow: ['^some-package$'] }],
errors: [
{
line: 1,
column: 13,
messageId: 'noVarReqs',
},
],
},
],
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f7b5ca8

Please sign in to comment.