From 906cd6bb4be7496f08b953e87e3f5540b2838358 Mon Sep 17 00:00:00 2001 From: "('3')" Date: Thu, 1 Jun 2023 03:22:01 +0900 Subject: [PATCH] Forbid re-exports of non-importable variables --- .../src/reexport4/filenameLoophole/sub.ts | 1 + .../src/reexport4/filenameLoophole/sub/foo.ts | 4 + .../src/reexport4/filenameLoophole/sub2.ts | 1 + .../indexLoophole/reexportFromSubFoo.ts | 1 + .../indexLoophole/reexportFromSubIndex.ts | 1 + .../src/reexport4/indexLoophole/sub/foo.ts | 4 + .../src/reexport4/indexLoophole/sub/index.ts | 4 + src/__tests__/reexport.ts | 89 +++++++++++++++++++ src/rules/jsdoc.ts | 66 ++++++++++++-- 9 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub.ts create mode 100644 src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub/foo.ts create mode 100644 src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub2.ts create mode 100644 src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubFoo.ts create mode 100644 src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubIndex.ts create mode 100644 src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/foo.ts create mode 100644 src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/index.ts diff --git a/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub.ts b/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub.ts new file mode 100644 index 0000000..de22441 --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub.ts @@ -0,0 +1 @@ +export { subFoo } from "./sub/foo"; diff --git a/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub/foo.ts b/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub/foo.ts new file mode 100644 index 0000000..6b502e2 --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub/foo.ts @@ -0,0 +1,4 @@ +/** + * @package + */ +export const subFoo = "hello!"; diff --git a/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub2.ts b/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub2.ts new file mode 100644 index 0000000..de22441 --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/filenameLoophole/sub2.ts @@ -0,0 +1 @@ +export { subFoo } from "./sub/foo"; diff --git a/src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubFoo.ts b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubFoo.ts new file mode 100644 index 0000000..de22441 --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubFoo.ts @@ -0,0 +1 @@ +export { subFoo } from "./sub/foo"; diff --git a/src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubIndex.ts b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubIndex.ts new file mode 100644 index 0000000..5fc083b --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/reexportFromSubIndex.ts @@ -0,0 +1 @@ +export { subFoo } from "./sub/index"; diff --git a/src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/foo.ts b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/foo.ts new file mode 100644 index 0000000..6b502e2 --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/foo.ts @@ -0,0 +1,4 @@ +/** + * @package + */ +export const subFoo = "hello!"; diff --git a/src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/index.ts b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/index.ts new file mode 100644 index 0000000..ebf59b6 --- /dev/null +++ b/src/__tests__/fixtures/project/src/reexport4/indexLoophole/sub/index.ts @@ -0,0 +1,4 @@ +/** + * @package + */ +export { subFoo } from "./foo"; diff --git a/src/__tests__/reexport.ts b/src/__tests__/reexport.ts index 5a3a9cc..3ee19e3 100644 --- a/src/__tests__/reexport.ts +++ b/src/__tests__/reexport.ts @@ -43,6 +43,32 @@ Array [ ] `); }); + it("Cannot re-export a package-private variable", async () => { + const result = await tester.lintFile( + "src/reexport4/indexLoophole/reexportFromSubFoo.ts" + ); + expect(result).toMatchInlineSnapshot(` +Array [ + Object { + "column": 10, + "endColumn": 16, + "endLine": 1, + "line": 1, + "message": "Cannot re-export a package-private export 'subFoo'", + "messageId": "package:reexport", + "nodeType": "ExportSpecifier", + "ruleId": "import-access/jsdoc", + "severity": 2, + }, +] +`); + }); + it("Can re-export a variable exported from index.ts", async () => { + const result = await tester.lintFile( + "src/reexport4/indexLoophole/reexportFromSubIndex.ts" + ); + expect(result).toMatchInlineSnapshot(`Array []`); + }); describe("indexLoophole = false", () => { it("Cannot import a package-private variable from sub/index.ts", async () => { const result = await tester.lintFile("src/reexport/useFoo.ts", { @@ -75,6 +101,31 @@ Array [ "severity": 2, }, ] +`); + }); + it("Cannot re-export a package-private variable", async () => { + const result = await tester.lintFile( + "src/reexport4/indexLoophole/reexportFromSubIndex.ts", + { + jsdoc: { + indexLoophole: false, + }, + } + ); + expect(result).toMatchInlineSnapshot(` +Array [ + Object { + "column": 10, + "endColumn": 16, + "endLine": 1, + "line": 1, + "message": "Cannot re-export a package-private export 'subFoo'", + "messageId": "package:reexport", + "nodeType": "ExportSpecifier", + "ruleId": "import-access/jsdoc", + "severity": 2, + }, +] `); }); }); @@ -100,6 +151,44 @@ Array [ "severity": 2, }, ] +`); + }); + it("Can re-export from sub directory of same name", async () => { + const result = await tester.lintFile( + "src/reexport4/filenameLoophole/sub.ts", + { + jsdoc: { + indexLoophole: false, + filenameLoophole: true, + }, + } + ); + expect(result).toMatchInlineSnapshot(`Array []`); + }); + it("Cannot re-export from sub directory of different name", async () => { + const result = await tester.lintFile( + "src/reexport4/filenameLoophole/sub2.ts", + { + jsdoc: { + indexLoophole: false, + filenameLoophole: true, + }, + } + ); + expect(result).toMatchInlineSnapshot(` +Array [ + Object { + "column": 10, + "endColumn": 16, + "endLine": 1, + "line": 1, + "message": "Cannot re-export a package-private export 'subFoo'", + "messageId": "package:reexport", + "nodeType": "ExportSpecifier", + "ruleId": "import-access/jsdoc", + "severity": 2, + }, +] `); }); }); diff --git a/src/rules/jsdoc.ts b/src/rules/jsdoc.ts index 3c0c4e8..179e85f 100644 --- a/src/rules/jsdoc.ts +++ b/src/rules/jsdoc.ts @@ -4,7 +4,11 @@ import { checkSymbolImportability } from "../core/checkSymbolmportability"; import { getImmediateAliasedSymbol } from "../utils/getImmediateAliasedSymbol"; import { PackageOptions } from "../utils/isInPackage"; -type MessageId = "package" | "private"; +type MessageId = + | "package" + | "package:reexport" + | "private" + | "private:reexport"; export type JSDocRuleOptions = { /** @@ -34,7 +38,11 @@ const jsdocRule: Omit< }, messages: { package: "Cannot import a package-private export '{{ identifier }}'", + "package:reexport": + "Cannot re-export a package-private export '{{ identifier }}'", private: "Cannot import a private export '{{ identifier }}'", + "private:reexport": + "Cannot re-export a private export '{{ identifier }}'", }, schema: [ { @@ -102,6 +110,29 @@ const jsdocRule: Omit< checkSymbol(context, packageOptions, checker, node, tsNode, symbol); } }, + ExportSpecifier(node) { + const shouldSkip = shouldSkipSymbolCheck(node); + if (shouldSkip) { + return; + } + + const checker = parserServices.program.getTypeChecker(); + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + + const symbol = checker.getSymbolAtLocation(tsNode.name); + if (symbol) { + checkSymbol( + context, + packageOptions, + checker, + node, + tsNode, + symbol, + true + ); + } + }, }; }, }; @@ -120,12 +151,28 @@ export function jsDocRuleDefaultOptions( } function shouldSkipSymbolCheck( - node: TSESTree.ImportSpecifier | TSESTree.ImportDefaultSpecifier -) { - if (node.parent?.type === "ImportDeclaration") { - const packageName = node.parent.source.value; - return isNodeBuiltinModule(packageName) || willBeImportedFromNodeModules(packageName); + node: + | TSESTree.ImportSpecifier + | TSESTree.ImportDefaultSpecifier + | TSESTree.ExportSpecifier +): boolean { + if (!node.parent) { + return true; } + if ( + node.parent.type !== "ImportDeclaration" && + node.parent.type !== "ExportNamedDeclaration" + ) { + return true; + } + const packageName = node.parent.source?.value; + if (!packageName) { + return true; + } + return ( + isNodeBuiltinModule(packageName) || + willBeImportedFromNodeModules(packageName) + ); } function isNodeBuiltinModule(importPath: string) { @@ -163,7 +210,8 @@ function checkSymbol( checker: TypeChecker, originalNode: TSESTree.Node, tsNode: Node, - symbol: Symbol + symbol: Symbol, + reexport = false ) { const exsy = getImmediateAliasedSymbol(checker, symbol); if (!exsy) { @@ -179,7 +227,7 @@ function checkSymbol( case "package": { context.report({ node: originalNode, - messageId: "package", + messageId: reexport ? "package:reexport" : "package", data: { identifier: exsy.name, }, @@ -189,7 +237,7 @@ function checkSymbol( case "private": { context.report({ node: originalNode, - messageId: "private", + messageId: reexport ? "private:reexport" : "private", data: { identifier: exsy.name, },