Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,34 @@
"local/debug-assert": "error",
"local/no-keywords": "error",
"local/jsdoc-format": "error",
"local/prefer-direct-import": "error",

// eslint-plugin-no-null
"no-null/no-null": "error",

// eslint-plugin-simple-import-sort
"simple-import-sort/imports": "error",
"simple-import-sort/imports": [
"error",
{
"groups": [
// Side effect imports.
["^\\u0000"],
// Node.js builtins prefixed with `node:`.
["^node:"],
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
["^@?\\w"],
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group.
["^"],
// Namespace barrels, which help dictate code execution order.
["_namespaces"],
// Relative imports.
// Anything that starts with a dot.
["^\\."]
]
}
],
"simple-import-sort/exports": "error"
},
"overrides": [
Expand Down
128 changes: 128 additions & 0 deletions scripts/eslint/rules/prefer-direct-import.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const { AST_NODE_TYPES } = require("@typescript-eslint/utils");
const { createRule } = require("./utils.cjs");
const path = require("path");

const srcRoot = path.resolve(__dirname, "../../../src");

const tsNamespaceBarrelRegex = /_namespaces\/ts(?:\.ts|\.js|\.mts|\.mjs|\.cts|\.cjs)?$/;

/**
* @type {Array<{ name: string; path: string; }>}
*/
const modules = [
// {
// name: "Debug",
// path: "compiler/debug",
// },
{
name: "Diagnostics",
path: "compiler/diagnosticInformationMap.generated",
},
];

module.exports = createRule({
name: "prefer-direct-import",
meta: {
docs: {
description: ``,
},
messages: {
importError: `{{ name }} should be imported directly from {{ path }}`,
},
schema: [],
type: "problem",
fixable: "code",
},
defaultOptions: [],

create(context) {
/**
* @param {string} p
*/
function getImportPath(p) {
let out = path.relative(path.dirname(context.filename), path.join(srcRoot, p)).replace(/\\/g, "/");
if (!out.startsWith(".")) out = "./" + out;
return out;
}

/** @type {any} */
let program;
let addedImport = false;

return {
Program: node => {
program = node;
},
ImportDeclaration: node => {
if (node.importKind !== "value" || !tsNamespaceBarrelRegex.test(node.source.value)) return;

for (const specifier of node.specifiers) {
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier || specifier.importKind !== "value") continue;

for (const mod of modules) {
if (specifier.imported.name !== mod.name) continue;

context.report({
messageId: "importError",
data: { name: mod.name, path: mod.path },
node: specifier,
fix: fixer => {
const newCode = `import * as ${mod.name} from "${getImportPath(mod.path)}";`;
const fixes = [];
if (node.specifiers.length === 1) {
if (addedImport) {
fixes.push(fixer.remove(node));
}
else {
fixes.push(fixer.replaceText(node, newCode));
addedImport = true;
}
}
else {
const comma = context.sourceCode.getTokenAfter(specifier, token => token.value === ",");
if (!comma) throw new Error("comma is null");
const prevNode = context.sourceCode.getTokenBefore(specifier);
if (!prevNode) throw new Error("prevNode is null");
fixes.push(
fixer.removeRange([prevNode.range[1], specifier.range[0]]),
fixer.remove(specifier),
fixer.remove(comma),
);
if (!addedImport) {
fixes.push(fixer.insertTextBefore(node, newCode + "\r\n"));
addedImport = true;
}
}

return fixes;
},
});
}
}
},
MemberExpression: node => {
if (node.object.type !== AST_NODE_TYPES.Identifier || node.object.name !== "ts") return;

for (const mod of modules) {
if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== mod.name) continue;

context.report({
messageId: "importError",
data: { name: mod.name, path: mod.path },
node,
fix: fixer => {
const fixes = [fixer.replaceText(node, mod.name)];

if (!addedImport) {
fixes.push(fixer.insertTextBefore(program, `import * as ${mod.name} from "${getImportPath(mod.path)}";\r\n`));
addedImport = true;
}

return fixes;
},
});
}
},
};
},
});
78 changes: 78 additions & 0 deletions scripts/eslint/tests/prefer-direct-import.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// const { RuleTester } = require("./support/RuleTester.cjs");
// const rule = require("../rules/prefer-direct-import.cjs");

// const ruleTester = new RuleTester({
// parserOptions: {
// warnOnUnsupportedTypeScriptVersion: false,
// },
// parser: require.resolve("@typescript-eslint/parser"),
// });

// ruleTester.run("no-ts-debug", rule, {
// valid: [
// {
// code: `
// import * as Debug from "./debug";
// `,
// },
// {
// code: `
// import type { Debug } from "./_namespaces/ts";
// `,
// },
// {
// code: `
// import { type Debug } from "./_namespaces/ts";
// `,
// },
// ],

// invalid: [
// {
// filename: "src/compiler/checker.ts",
// code: `
// import { Debug } from "./_namespaces/ts";
// `.replace(/\r?\n/g, "\r\n"),
// errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
// output: `
// import * as Debug from "./debug";
// `.replace(/\r?\n/g, "\r\n"),
// },
// {
// filename: "src/compiler/transformers/ts.ts",
// code: `
// import { Debug } from "../_namespaces/ts";
// `.replace(/\r?\n/g, "\r\n"),
// errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
// output: `
// import * as Debug from "../debug";
// `.replace(/\r?\n/g, "\r\n"),
// },
// // TODO(jakebailey): the rule probably needs to handle .js extensions
// {
// filename: "src/compiler/checker.ts",
// code: `
// import { Debug } from "./_namespaces/ts.js";
// `.replace(/\r?\n/g, "\r\n"),
// errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
// output: `
// import * as Debug from "./debug";
// `.replace(/\r?\n/g, "\r\n"),
// },
// {
// filename: "src/compiler/checker.ts",
// code: `
// import * as ts from "./_namespaces/ts";

// ts.Debug.assert(true);
// `.replace(/\r?\n/g, "\r\n"),
// errors: [{ messageId: "importError", data: { name: "Debug", path: "compiler/debug" } }],
// output: `
// import * as Debug from "./debug";
// import * as ts from "./_namespaces/ts";

// Debug.assert(true);
// `.replace(/\r?\n/g, "\r\n"),
// },
// ],
// });
27 changes: 22 additions & 5 deletions scripts/processDiagnosticMessages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ function checkForUniqueCodes(diagnosticTable) {
});
}

/**
* @param {string} category
*/
function categoryToNumber(category) {
// Copy of DiagnosticCategory enum from src/compiler/types.ts.
switch (category) {
case "Warning":
return 0;
case "Error":
return 1;
case "Suggestion":
return 2;
case "Message":
return 3;
default:
throw new Error(`Unknown category ${category}`);
}
}

/**
* @param {InputDiagnosticMessageTable} messageTable
* @param {string} inputFilePathRel
Expand All @@ -85,27 +104,25 @@ function buildInfoFileOutput(messageTable, inputFilePathRel) {
const result = [
"// <auto-generated />",
`// generated from '${inputFilePathRel}'`,
"/* eslint-disable @typescript-eslint/naming-convention */",
"",
'import { DiagnosticCategory, DiagnosticMessage } from "./types";',
"",
"function diag(code: number, category: DiagnosticCategory, key: string, message: string, reportsUnnecessary?: {}, elidedInCompatabilityPyramid?: boolean, reportsDeprecated?: {}): DiagnosticMessage {",
" return { code, category, key, message, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated };",
"}",
"",
"/** @internal */",
"export const Diagnostics = {",
];
messageTable.forEach(({ code, category, reportsUnnecessary, elidedInCompatabilityPyramid, reportsDeprecated }, name) => {
const propName = convertPropertyName(name);
const argReportsUnnecessary = reportsUnnecessary ? `, /*reportsUnnecessary*/ ${reportsUnnecessary}` : "";
const argElidedInCompatabilityPyramid = elidedInCompatabilityPyramid ? `${!reportsUnnecessary ? ", /*reportsUnnecessary*/ undefined" : ""}, /*elidedInCompatabilityPyramid*/ ${elidedInCompatabilityPyramid}` : "";
const argReportsDeprecated = reportsDeprecated ? `${!argElidedInCompatabilityPyramid ? ", /*reportsUnnecessary*/ undefined, /*elidedInCompatabilityPyramid*/ undefined" : ""}, /*reportsDeprecated*/ ${reportsDeprecated}` : "";

result.push(` ${propName}: diag(${code}, DiagnosticCategory.${category}, "${createKey(propName, code)}", ${JSON.stringify(name)}${argReportsUnnecessary}${argElidedInCompatabilityPyramid}${argReportsDeprecated}),`);
result.push("/** @internal */");
result.push(`export const ${propName}: DiagnosticMessage = /* @__PURE__ */ diag(${code}, ${categoryToNumber(category)} satisfies DiagnosticCategory.${category}, "${createKey(propName, code)}", ${JSON.stringify(name)}${argReportsUnnecessary}${argElidedInCompatabilityPyramid}${argReportsDeprecated});`);
});

result.push("};");

return result.join("\r\n");
}

Expand Down
6 changes: 6 additions & 0 deletions src/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
"rules": {
"no-restricted-globals": "off"
}
},
{
"files": ["harness/**", "testRunner/**", "tsserver/**", "typingsInstaller/**"],
"rules": {
"local/prefer-direct-import": "off"
}
}
]
}
3 changes: 2 additions & 1 deletion src/compiler/_namespaces/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export * from "../tracing";
export * from "../types";
export * from "../sys";
export * from "../path";
export * from "../diagnosticInformationMap.generated";
import * as Diagnostics from "../diagnosticInformationMap.generated";
export { Diagnostics };
export * from "../scanner";
export * from "../utilitiesPublic";
export * from "../utilities";
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import {
DiagnosticCategory,
DiagnosticMessage,
DiagnosticRelatedInformation,
Diagnostics,
DiagnosticWithLocation,
DoStatement,
DynamicNamedDeclaration,
Expand Down Expand Up @@ -318,6 +317,8 @@ import {
} from "./_namespaces/ts";
import * as performance from "./_namespaces/ts.performance";

import * as Diagnostics from "./diagnosticInformationMap.generated";

/** @internal */
export const enum ModuleInstanceState {
NonInstantiated = 0,
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ import {
DiagnosticMessage,
DiagnosticMessageChain,
DiagnosticRelatedInformation,
Diagnostics,
DiagnosticWithLocation,
DoStatement,
DynamicNamedDeclaration,
Expand Down Expand Up @@ -1092,6 +1091,8 @@ import {
import * as moduleSpecifiers from "./_namespaces/ts.moduleSpecifiers";
import * as performance from "./_namespaces/ts.performance";

import * as Diagnostics from "./diagnosticInformationMap.generated";

const ambientModuleSymbolRegex = /^".+"$/;
const anon = "(anonymous)" as __String & string;

Expand Down
3 changes: 2 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
Diagnostic,
DiagnosticArguments,
DiagnosticMessage,
Diagnostics,
DidYouMeanOptionsDiagnostics,
directorySeparator,
emptyArray,
Expand Down Expand Up @@ -122,6 +121,8 @@ import {
WatchOptions,
} from "./_namespaces/ts";

import * as Diagnostics from "./diagnosticInformationMap.generated";

/** @internal */
export const compileOnSaveCommandLineOption: CommandLineOption = {
name: "compileOnSave",
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
Diagnostic,
DiagnosticMessage,
DiagnosticReporter,
Diagnostics,
directoryProbablyExists,
directorySeparator,
emptyArray,
Expand Down Expand Up @@ -110,6 +109,8 @@ import {
VersionRange,
} from "./_namespaces/ts";

import * as Diagnostics from "./diagnosticInformationMap.generated";

/** @internal */
export function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void {
host.trace!(formatMessage(message, ...args));
Expand Down
Loading