Skip to content

Commit f98670d

Browse files
authored
Forbid re-exports of non-importable variables (#9)
1 parent 05dc4a6 commit f98670d

File tree

9 files changed

+162
-9
lines changed

9 files changed

+162
-9
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { subFoo } from "./sub/foo";
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* @package
3+
*/
4+
export const subFoo = "hello!";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { subFoo } from "./sub/foo";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { subFoo } from "./sub/foo";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { subFoo } from "./sub/index";
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* @package
3+
*/
4+
export const subFoo = "hello!";
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* @package
3+
*/
4+
export { subFoo } from "./foo";

src/__tests__/reexport.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ Array [
4343
]
4444
`);
4545
});
46+
it("Cannot re-export a package-private variable", async () => {
47+
const result = await tester.lintFile(
48+
"src/reexport4/indexLoophole/reexportFromSubFoo.ts"
49+
);
50+
expect(result).toMatchInlineSnapshot(`
51+
Array [
52+
Object {
53+
"column": 10,
54+
"endColumn": 16,
55+
"endLine": 1,
56+
"line": 1,
57+
"message": "Cannot re-export a package-private export 'subFoo'",
58+
"messageId": "package:reexport",
59+
"nodeType": "ExportSpecifier",
60+
"ruleId": "import-access/jsdoc",
61+
"severity": 2,
62+
},
63+
]
64+
`);
65+
});
66+
it("Can re-export a variable exported from index.ts", async () => {
67+
const result = await tester.lintFile(
68+
"src/reexport4/indexLoophole/reexportFromSubIndex.ts"
69+
);
70+
expect(result).toMatchInlineSnapshot(`Array []`);
71+
});
4672
describe("indexLoophole = false", () => {
4773
it("Cannot import a package-private variable from sub/index.ts", async () => {
4874
const result = await tester.lintFile("src/reexport/useFoo.ts", {
@@ -75,6 +101,31 @@ Array [
75101
"severity": 2,
76102
},
77103
]
104+
`);
105+
});
106+
it("Cannot re-export a package-private variable", async () => {
107+
const result = await tester.lintFile(
108+
"src/reexport4/indexLoophole/reexportFromSubIndex.ts",
109+
{
110+
jsdoc: {
111+
indexLoophole: false,
112+
},
113+
}
114+
);
115+
expect(result).toMatchInlineSnapshot(`
116+
Array [
117+
Object {
118+
"column": 10,
119+
"endColumn": 16,
120+
"endLine": 1,
121+
"line": 1,
122+
"message": "Cannot re-export a package-private export 'subFoo'",
123+
"messageId": "package:reexport",
124+
"nodeType": "ExportSpecifier",
125+
"ruleId": "import-access/jsdoc",
126+
"severity": 2,
127+
},
128+
]
78129
`);
79130
});
80131
});
@@ -100,6 +151,44 @@ Array [
100151
"severity": 2,
101152
},
102153
]
154+
`);
155+
});
156+
it("Can re-export from sub directory of same name", async () => {
157+
const result = await tester.lintFile(
158+
"src/reexport4/filenameLoophole/sub.ts",
159+
{
160+
jsdoc: {
161+
indexLoophole: false,
162+
filenameLoophole: true,
163+
},
164+
}
165+
);
166+
expect(result).toMatchInlineSnapshot(`Array []`);
167+
});
168+
it("Cannot re-export from sub directory of different name", async () => {
169+
const result = await tester.lintFile(
170+
"src/reexport4/filenameLoophole/sub2.ts",
171+
{
172+
jsdoc: {
173+
indexLoophole: false,
174+
filenameLoophole: true,
175+
},
176+
}
177+
);
178+
expect(result).toMatchInlineSnapshot(`
179+
Array [
180+
Object {
181+
"column": 10,
182+
"endColumn": 16,
183+
"endLine": 1,
184+
"line": 1,
185+
"message": "Cannot re-export a package-private export 'subFoo'",
186+
"messageId": "package:reexport",
187+
"nodeType": "ExportSpecifier",
188+
"ruleId": "import-access/jsdoc",
189+
"severity": 2,
190+
},
191+
]
103192
`);
104193
});
105194
});

src/rules/jsdoc.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { checkSymbolImportability } from "../core/checkSymbolmportability";
44
import { getImmediateAliasedSymbol } from "../utils/getImmediateAliasedSymbol";
55
import { PackageOptions } from "../utils/isInPackage";
66

7-
type MessageId = "package" | "private";
7+
type MessageId =
8+
| "package"
9+
| "package:reexport"
10+
| "private"
11+
| "private:reexport";
812

913
export type JSDocRuleOptions = {
1014
/**
@@ -34,7 +38,11 @@ const jsdocRule: Omit<
3438
},
3539
messages: {
3640
package: "Cannot import a package-private export '{{ identifier }}'",
41+
"package:reexport":
42+
"Cannot re-export a package-private export '{{ identifier }}'",
3743
private: "Cannot import a private export '{{ identifier }}'",
44+
"private:reexport":
45+
"Cannot re-export a private export '{{ identifier }}'",
3846
},
3947
schema: [
4048
{
@@ -102,6 +110,29 @@ const jsdocRule: Omit<
102110
checkSymbol(context, packageOptions, checker, node, tsNode, symbol);
103111
}
104112
},
113+
ExportSpecifier(node) {
114+
const shouldSkip = shouldSkipSymbolCheck(node);
115+
if (shouldSkip) {
116+
return;
117+
}
118+
119+
const checker = parserServices.program.getTypeChecker();
120+
121+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
122+
123+
const symbol = checker.getSymbolAtLocation(tsNode.name);
124+
if (symbol) {
125+
checkSymbol(
126+
context,
127+
packageOptions,
128+
checker,
129+
node,
130+
tsNode,
131+
symbol,
132+
true
133+
);
134+
}
135+
},
105136
};
106137
},
107138
};
@@ -120,12 +151,28 @@ export function jsDocRuleDefaultOptions(
120151
}
121152

122153
function shouldSkipSymbolCheck(
123-
node: TSESTree.ImportSpecifier | TSESTree.ImportDefaultSpecifier
124-
) {
125-
if (node.parent?.type === "ImportDeclaration") {
126-
const packageName = node.parent.source.value;
127-
return isNodeBuiltinModule(packageName) || willBeImportedFromNodeModules(packageName);
154+
node:
155+
| TSESTree.ImportSpecifier
156+
| TSESTree.ImportDefaultSpecifier
157+
| TSESTree.ExportSpecifier
158+
): boolean {
159+
if (!node.parent) {
160+
return true;
128161
}
162+
if (
163+
node.parent.type !== "ImportDeclaration" &&
164+
node.parent.type !== "ExportNamedDeclaration"
165+
) {
166+
return true;
167+
}
168+
const packageName = node.parent.source?.value;
169+
if (!packageName) {
170+
return true;
171+
}
172+
return (
173+
isNodeBuiltinModule(packageName) ||
174+
willBeImportedFromNodeModules(packageName)
175+
);
129176
}
130177

131178
function isNodeBuiltinModule(importPath: string) {
@@ -163,7 +210,8 @@ function checkSymbol(
163210
checker: TypeChecker,
164211
originalNode: TSESTree.Node,
165212
tsNode: Node,
166-
symbol: Symbol
213+
symbol: Symbol,
214+
reexport = false
167215
) {
168216
const exsy = getImmediateAliasedSymbol(checker, symbol);
169217
if (!exsy) {
@@ -179,7 +227,7 @@ function checkSymbol(
179227
case "package": {
180228
context.report({
181229
node: originalNode,
182-
messageId: "package",
230+
messageId: reexport ? "package:reexport" : "package",
183231
data: {
184232
identifier: exsy.name,
185233
},
@@ -189,7 +237,7 @@ function checkSymbol(
189237
case "private": {
190238
context.report({
191239
node: originalNode,
192-
messageId: "private",
240+
messageId: reexport ? "private:reexport" : "private",
193241
data: {
194242
identifier: exsy.name,
195243
},

0 commit comments

Comments
 (0)