Skip to content

Commit cb0d8ac

Browse files
authored
Merge pull request #29 from buildo/no-module-imports
Add no-module-imports
2 parents 8524985 + 166c028 commit cb0d8ac

13 files changed

+746
-187
lines changed

README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ and enable the rules you want, for example
3737
## List of supported rules
3838

3939
- [fp-ts/no-lib-imports](docs/rules/no-lib-imports.md): Disallow imports from
40-
'fp-ts/lib'
40+
'fp-ts/lib' (autofixable 🔧)
4141
- [fp-ts/no-pipeable](docs/rules/no-pipeable.md): Disallow imports from the
42-
'pipeable' module
42+
'pipeable' module (autofixable 🔧)
4343
- [fp-ts/prefer-traverse](docs/rules/prefer-traverse.md): Replace map + sequence
44-
with traverse
44+
with traverse (autofixable 🔧)
4545
- [fp-ts/no-redundant-flow](docs/rules/no-redundant-flow.md): Remove redundant
46-
uses of flow
46+
uses of flow (autofixable 🔧)
4747
- [fp-ts/prefer-chain](docs/rules/prefer-chain.md): Replace map + flatten with
48-
chain
48+
chain (autofixable 🔧)
49+
- [fp-ts/no-module-imports](docs/rules/no-module-imports.md): Disallow imports
50+
from fp-ts modules (autofixable 🔧)
4951

5052
## Configurations
5153

docs/rules/no-module-imports.md

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Disallow imports from fp-ts modules (fp-ts/no-module-imports)
2+
3+
Disallow imports from fp-ts modules, such as `fp-ts/Option`.
4+
5+
The `function` module is an exception and it's allowed nonetheless, since it's
6+
not exported from fp-ts's index.
7+
8+
**Fixable**: This rule is automatically fixable using the `--fix` flag on the
9+
command line.
10+
11+
## Rule Details
12+
13+
Possible configurations:
14+
15+
- `"always"`: disallow importing any member from a module. This is the default
16+
value.
17+
- `"allow-types"`: allow importing type members from a module.
18+
19+
Example of **incorrect** code for this rule, when configured as `always`:
20+
21+
```ts
22+
import { Option, some } from "fp-ts/Option";
23+
24+
const x: Option<number> = some(42);
25+
```
26+
27+
Example of **incorrect** code for this rule, when configured as `allow-types`:
28+
29+
```ts
30+
import { some } from "fp-ts/Option";
31+
32+
const x = some(42);
33+
```
34+
35+
Example of **correct** code for this rule, when configured as `always`:
36+
37+
```ts
38+
import { option } from "fp-ts";
39+
40+
const x: option.Option<number> = option.some(42);
41+
```
42+
43+
Example of **correct** code for this rule, when configured as `allow-types`:
44+
45+
```ts
46+
import { option } from "fp-ts";
47+
import { Option } from "fp-ts/Option";
48+
49+
const x: Option<number> = option.some(42);
50+
```

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@
2626
"dependencies": {
2727
"@typescript-eslint/experimental-utils": "^4.11.1",
2828
"astring": "^1.4.3",
29+
"estraverse": "^5.2.0",
2930
"fp-ts": "^2.9.3"
3031
},
3132
"devDependencies": {
3233
"@types/astring": "^1.3.0",
34+
"@types/common-tags": "^1.8.0",
35+
"@types/estraverse": "^5.1.0",
3336
"@types/node": "^14.14.16",
3437
"@types/requireindex": "^1.2.0",
3538
"@typescript-eslint/parser": "^4.11.1",
39+
"common-tags": "^1.8.0",
3640
"eslint": "^7.17.0",
3741
"eslint-plugin-fp-ts": "^0.1.8",
3842
"jest": "^26.6.3",

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { pipe } from "fp-ts/function";
44
const potentialErrors = {
55
"no-lib-imports": require("./rules/no-lib-imports"),
66
"no-pipeable": require("./rules/no-pipeable"),
7+
"no-module-imports": require("./rules/no-module-imports"),
78
};
89

910
const suggestions = {

src/rules/no-lib-imports.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ASTUtils, TSESLint } from "@typescript-eslint/experimental-utils";
1+
import { TSESLint } from "@typescript-eslint/experimental-utils";
22

33
const messages = {
44
importNotAllowed:
@@ -18,7 +18,7 @@ export function create(
1818
): TSESLint.RuleListener {
1919
return {
2020
ImportDeclaration(node) {
21-
const sourceValue = ASTUtils.getStringIfConstant(node.source);
21+
const sourceValue = node.source.value?.toString();
2222
if (sourceValue) {
2323
const forbiddenImportPattern = /^fp-ts\/lib\//;
2424

src/rules/no-module-imports.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {
2+
ASTUtils,
3+
AST_NODE_TYPES,
4+
TSESLint,
5+
} from "@typescript-eslint/experimental-utils";
6+
import { array } from "fp-ts";
7+
import { pipe } from "fp-ts/function";
8+
import { contextUtils } from "../utils";
9+
10+
const messages = {
11+
importNotAllowed:
12+
"Importing from modules is not allowed, import from 'fp-ts' instead",
13+
importValuesNotAllowed:
14+
"Importing values from modules is not allowed, import from 'fp-ts' instead",
15+
convertImportToIndex: "Import all members from fp-ts",
16+
convertImportValuesToIndex: "Import values from fp-ts",
17+
} as const;
18+
type MessageIds = keyof typeof messages;
19+
20+
type Options = ["always" | "allow-types"];
21+
22+
export const meta: TSESLint.RuleMetaData<MessageIds> = {
23+
type: "suggestion",
24+
fixable: "code",
25+
schema: [
26+
{
27+
enum: ["always", "allow-types"],
28+
},
29+
],
30+
messages,
31+
};
32+
33+
export function create(
34+
context: TSESLint.RuleContext<MessageIds, Options>
35+
): TSESLint.RuleListener {
36+
const allowTypes = context.options[0] === "allow-types";
37+
const allowedModules = ["function"];
38+
39+
const {
40+
addNamedImportIfNeeded,
41+
isOnlyUsedAsType,
42+
removeImportDeclaration,
43+
} = contextUtils(context);
44+
45+
return {
46+
ImportDeclaration(node) {
47+
const sourceValue = node.source.value?.toString();
48+
if (sourceValue) {
49+
const forbiddenImportPattern = /^fp-ts\/(.+)/;
50+
const matches = sourceValue.match(forbiddenImportPattern);
51+
52+
if (matches != null) {
53+
const matchedModule = matches[1]!.replace("lib/", "");
54+
if (allowedModules.includes(matchedModule)) {
55+
return;
56+
}
57+
58+
const importSpecifiers = node.specifiers.filter(
59+
(importClause) =>
60+
importClause.type === AST_NODE_TYPES.ImportSpecifier
61+
);
62+
63+
const nonTypeImports = pipe(
64+
importSpecifiers,
65+
array.filter((i) => !isOnlyUsedAsType(i))
66+
);
67+
68+
if (allowTypes && nonTypeImports.length === 0) {
69+
return;
70+
}
71+
72+
if (importSpecifiers.length > 0) {
73+
context.report({
74+
node: node.source,
75+
messageId: allowTypes
76+
? "importValuesNotAllowed"
77+
: "importNotAllowed",
78+
suggest: [
79+
{
80+
messageId: allowTypes
81+
? "convertImportValuesToIndex"
82+
: "convertImportToIndex",
83+
fix(fixer) {
84+
const indexExport =
85+
matchedModule.charAt(0).toLowerCase() +
86+
matchedModule.slice(1);
87+
88+
const referencesFixes = importSpecifiers.flatMap(
89+
(importSpecifier) => {
90+
const variable = ASTUtils.findVariable(
91+
context.getScope(),
92+
importSpecifier.local.name
93+
);
94+
if (variable) {
95+
return variable.references
96+
.filter((ref) =>
97+
allowTypes
98+
? ref.identifier.parent?.type !==
99+
AST_NODE_TYPES.TSTypeReference
100+
: true
101+
)
102+
.filter(
103+
(ref) =>
104+
ref.identifier.parent?.type !==
105+
AST_NODE_TYPES.MemberExpression
106+
)
107+
.map((ref) =>
108+
fixer.insertTextBefore(
109+
ref.identifier,
110+
`${indexExport}.`
111+
)
112+
);
113+
} else {
114+
return [];
115+
}
116+
}
117+
);
118+
119+
const importFixes =
120+
!allowTypes ||
121+
nonTypeImports.length === importSpecifiers.length
122+
? [removeImportDeclaration(node, fixer)]
123+
: nonTypeImports.map((node) => {
124+
if (
125+
context.getSourceCode().getTokenAfter(node)
126+
?.value === ","
127+
) {
128+
return fixer.removeRange([
129+
node.range[0],
130+
node.range[1] + 1,
131+
]);
132+
} else {
133+
return fixer.remove(node);
134+
}
135+
});
136+
137+
return [
138+
...importFixes,
139+
...addNamedImportIfNeeded(indexExport, "fp-ts", fixer),
140+
...referencesFixes,
141+
];
142+
},
143+
},
144+
],
145+
});
146+
}
147+
}
148+
}
149+
},
150+
};
151+
}

src/rules/no-redundant-flow.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { TSESLint } from "@typescript-eslint/experimental-utils";
2-
import { isFlowExpression } from "../utils";
2+
import { contextUtils } from "../utils";
33

44
const messages = {
55
redundantFlow: "flow can be removed because it takes only one argument",
@@ -17,9 +17,11 @@ export const meta: TSESLint.RuleMetaData<MessageIds> = {
1717
export function create(
1818
context: TSESLint.RuleContext<MessageIds, []>
1919
): TSESLint.RuleListener {
20+
const { isFlowExpression } = contextUtils(context);
21+
2022
return {
2123
CallExpression(node) {
22-
if (node.arguments.length === 1 && isFlowExpression(node, context)) {
24+
if (node.arguments.length === 1 && isFlowExpression(node)) {
2325
context.report({
2426
node,
2527
messageId: "redundantFlow",

0 commit comments

Comments
 (0)