Skip to content

Commit 3ed597f

Browse files
committed
Switch to module and make lint rule
1 parent cb98634 commit 3ed597f

File tree

17 files changed

+1262
-1003
lines changed

17 files changed

+1262
-1003
lines changed

eslint.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export default tseslint.config(
162162
"local/no-keywords": "error",
163163
"local/jsdoc-format": "error",
164164
"local/js-extensions": "error",
165+
"local/prefer-direct-import": "error",
165166
},
166167
},
167168
{
@@ -209,6 +210,12 @@ export default tseslint.config(
209210
"no-restricted-globals": "off",
210211
},
211212
},
213+
{
214+
files: ["src/harness/**", "src/testRunner/**", "src/tsserver/**", "src/typingsInstaller/**"],
215+
rules: {
216+
"local/prefer-direct-import": "off",
217+
},
218+
},
212219
{
213220
files: ["src/lib/*.d.ts"],
214221
...tseslint.configs.disableTypeChecked,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
const { AST_NODE_TYPES } = require("@typescript-eslint/utils");
2+
const { createRule } = require("./utils.cjs");
3+
const path = require("path");
4+
5+
const srcRoot = path.resolve(__dirname, "../../../src");
6+
7+
const tsNamespaceBarrelRegex = /_namespaces\/ts(?:\.ts|\.js|\.mts|\.mjs|\.cts|\.cjs)?$/;
8+
9+
/**
10+
* @type {Array<{ name: string; path: string; }>}
11+
*/
12+
const modules = [
13+
{
14+
name: "Debug",
15+
path: "compiler/debug",
16+
},
17+
];
18+
19+
module.exports = createRule({
20+
name: "prefer-direct-import",
21+
meta: {
22+
docs: {
23+
description: ``,
24+
},
25+
messages: {
26+
importError: `{{ name }} should be imported directly from {{ path }}`,
27+
},
28+
schema: [],
29+
type: "problem",
30+
fixable: "code",
31+
},
32+
defaultOptions: [],
33+
34+
create(context) {
35+
/**
36+
* @param {string} p
37+
*/
38+
function getImportPath(p) {
39+
let out = path.relative(path.dirname(context.filename), path.join(srcRoot, p)).replace(/\\/g, "/");
40+
if (!out.startsWith(".")) out = "./" + out;
41+
return out;
42+
}
43+
44+
/** @type {any} */
45+
let program;
46+
let addedImport = false;
47+
48+
return {
49+
Program: node => {
50+
program = node;
51+
},
52+
ImportDeclaration: node => {
53+
if (node.importKind !== "value" || !tsNamespaceBarrelRegex.test(node.source.value)) return;
54+
55+
for (const specifier of node.specifiers) {
56+
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier || specifier.importKind !== "value") continue;
57+
58+
for (const mod of modules) {
59+
if (specifier.imported.name !== mod.name) continue;
60+
61+
context.report({
62+
messageId: "importError",
63+
data: { name: mod.name, path: mod.path },
64+
node: specifier,
65+
fix: fixer => {
66+
const newCode = `import * as ${mod.name} from "${getImportPath(mod.path)}";`;
67+
const fixes = [];
68+
if (node.specifiers.length === 1) {
69+
if (addedImport) {
70+
fixes.push(fixer.remove(node));
71+
}
72+
else {
73+
fixes.push(fixer.replaceText(node, newCode));
74+
addedImport = true;
75+
}
76+
}
77+
else {
78+
const comma = context.sourceCode.getTokenAfter(specifier, token => token.value === ",");
79+
if (!comma) throw new Error("comma is null");
80+
const prevNode = context.sourceCode.getTokenBefore(specifier);
81+
if (!prevNode) throw new Error("prevNode is null");
82+
fixes.push(
83+
fixer.removeRange([prevNode.range[1], specifier.range[0]]),
84+
fixer.remove(specifier),
85+
fixer.remove(comma),
86+
);
87+
if (!addedImport) {
88+
fixes.push(fixer.insertTextBefore(node, newCode + "\r\n"));
89+
addedImport = true;
90+
}
91+
}
92+
93+
return fixes;
94+
},
95+
});
96+
}
97+
}
98+
},
99+
MemberExpression: node => {
100+
if (node.object.type !== AST_NODE_TYPES.Identifier || node.object.name !== "ts") return;
101+
102+
for (const mod of modules) {
103+
if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== mod.name) continue;
104+
105+
context.report({
106+
messageId: "importError",
107+
data: { name: mod.name, path: mod.path },
108+
node,
109+
fix: fixer => {
110+
const fixes = [fixer.replaceText(node, mod.name)];
111+
112+
if (!addedImport) {
113+
fixes.push(fixer.insertTextBefore(program, `import * as ${mod.name} from "${getImportPath(mod.path)}";\r\n`));
114+
addedImport = true;
115+
}
116+
117+
return fixes;
118+
},
119+
});
120+
}
121+
},
122+
};
123+
},
124+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const { RuleTester } = require("./support/RuleTester.cjs");
2+
const rule = require("../rules/prefer-direct-import.cjs");
3+
4+
const ruleTester = new RuleTester({
5+
parserOptions: {
6+
warnOnUnsupportedTypeScriptVersion: false,
7+
},
8+
parser: require.resolve("@typescript-eslint/parser"),
9+
});
10+
11+
ruleTester.run("no-ts-debug", rule, {
12+
valid: [
13+
{
14+
code: `
15+
import * as Debug from "./debug";
16+
`,
17+
},
18+
{
19+
code: `
20+
import type { Debug } from "./_namespaces/ts";
21+
`,
22+
},
23+
{
24+
code: `
25+
import { type Debug } from "./_namespaces/ts";
26+
`,
27+
},
28+
],
29+
30+
invalid: [
31+
{
32+
filename: "src/compiler/checker.ts",
33+
code: `
34+
import { Debug } from "./_namespaces/ts";
35+
`.replace(/\r?\n/g, "\r\n"),
36+
errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
37+
output: `
38+
import * as Debug from "./debug";
39+
`.replace(/\r?\n/g, "\r\n"),
40+
},
41+
{
42+
filename: "src/compiler/transformers/ts.ts",
43+
code: `
44+
import { Debug } from "../_namespaces/ts";
45+
`.replace(/\r?\n/g, "\r\n"),
46+
errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
47+
output: `
48+
import * as Debug from "../debug";
49+
`.replace(/\r?\n/g, "\r\n"),
50+
},
51+
// TODO(jakebailey): the rule probably needs to handle .js extensions
52+
{
53+
filename: "src/compiler/checker.ts",
54+
code: `
55+
import { Debug } from "./_namespaces/ts.js";
56+
`.replace(/\r?\n/g, "\r\n"),
57+
errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
58+
output: `
59+
import * as Debug from "./debug";
60+
`.replace(/\r?\n/g, "\r\n"),
61+
},
62+
{
63+
filename: "src/compiler/checker.ts",
64+
code: `
65+
import * as ts from "./_namespaces/ts";
66+
67+
ts.Debug.assert(true);
68+
`.replace(/\r?\n/g, "\r\n"),
69+
errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
70+
output: `
71+
import * as Debug from "./debug";
72+
import * as ts from "./_namespaces/ts";
73+
74+
Debug.assert(true);
75+
`.replace(/\r?\n/g, "\r\n"),
76+
},
77+
],
78+
});

src/compiler/_namespaces/ts.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
export * from "../corePublic.js";
44
export * from "../core.js";
5-
export * from "../debug.js";
5+
import * as Debug from "../debug.js";
6+
export { Debug };
67
export * from "../semver.js";
78
export * from "../performanceCore.js";
89
export * from "../tracing.js";

0 commit comments

Comments
 (0)