Skip to content

Commit 8d71a52

Browse files
authored
feat: add excludeSourcePatterns option to import-access/jsdoc rule (#125)
* feat: add excludeSourcePatterns option to import-access/jsdoc rule * docs: update rule-jsdoc.md to clarify pattern resolution relative to project root --------- Co-authored-by: ygkn <ygkn@users.noreply.github.com>
1 parent bd6f6e4 commit 8d71a52

File tree

9 files changed

+167
-5
lines changed

9 files changed

+167
-5
lines changed

docs/rule-jsdoc.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ As a bonus feature, importing exports annotated with `@private` is always forbid
1414
"indexLoophole": true,
1515
"filenameLoophole": false,
1616
"defaultImportability": "public", // "public" | "package" | "private"
17-
"treatSelfReferenceAs": "external" // "internal" | "external"
17+
"treatSelfReferenceAs": "external", // "internal" | "external"
18+
"excludeSourcePatterns": ["generated/**/*"] // Array of glob patterns for source paths to exclude
1819
}],
1920
}
2021
```
@@ -49,6 +50,7 @@ type JSDocRuleOptions = {
4950
filenameLoophole: boolean;
5051
defaultImportability: "public" | "pacakge" | "private";
5152
treatSelfReferenceAs: "internal" | "external";
53+
excludeSourcePatterns?: string[];
5254
};
5355
```
5456

@@ -186,3 +188,56 @@ In the above example, `my-package/foo` is a self reference that connects to `src
186188
When `treatSelfReferenceAs: external`, this import is always allowed even though `something` is a package-private export because it is treated like an import from an external package.
187189

188190
When `treatSelfReferenceAs: internal`, this import is disallowed because import from `my-package/foo` is treated like an import from `src/somewhere/foo.ts`.
191+
192+
### `excludeSourcePatterns`
193+
194+
_Default value: `[]`_
195+
196+
An array of glob patterns for source paths to exclude from the importability check. The patterns are resolved relative to the project root. When importing from a module that matches one of these patterns, the import-access/jsdoc rule will not apply any restrictions, regardless of the JSDoc annotations or defaultImportability setting.
197+
198+
This is particularly useful for handling imports from auto-generated files that don't have proper JSDoc annotations.
199+
200+
The patterns use the [minimatch](https://github.com/isaacs/minimatch) library's glob syntax.
201+
202+
**Examples:**
203+
204+
```ts
205+
// Example: Match by file path
206+
// excludeSourcePatterns: ["src/types/**/*.d.ts"]
207+
import { typeDefinition } from "../types/api"; // Allowed if the implementation is in a .d.ts file in the src/types directory
208+
```
209+
210+
Additional examples:
211+
```js
212+
// Match all files in a particular directory
213+
"src/generated/**"
214+
215+
// Match multiple extensions
216+
"**/*.{generated,auto}.{ts,js}"
217+
218+
// Match files with specific naming patterns
219+
"**/[a-z]*.auto.ts"
220+
221+
// Match specific type definition files
222+
"src/**/*.d.ts"
223+
```
224+
225+
226+
**Next.js specific configuration:**
227+
228+
When working with Next.js projects, it's recommended to exclude the `.next` directory which contains auto-generated files:
229+
230+
```js
231+
// In your .eslintrc.js
232+
{
233+
"rules": {
234+
"import-access/jsdoc": ["error", {
235+
// ... other options ...
236+
"excludeSourcePatterns": [".next/**"]
237+
}]
238+
}
239+
}
240+
```
241+
242+
This will ensure that any imports from auto-generated files in the `.next` directory are not subject to import-access restrictions.
243+

package-lock.json

Lines changed: 21 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"license": "MIT",
3636
"dependencies": {
3737
"@typescript-eslint/utils": "^8.4.0",
38+
"minimatch": "^10.0.1",
3839
"tsutils": "^3.21.0"
3940
},
4041
"devDependencies": {

src/__tests__/exclude-patterns.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getESLintTester } from "./fixtures/eslint";
2+
3+
const tester = getESLintTester();
4+
5+
it("Importing from generated package is disallowed by default", async () => {
6+
const result = await tester.lintFile(
7+
"src/exclude-patterns/generated-type-user.ts",
8+
{
9+
jsdoc: {
10+
defaultImportability: "package",
11+
},
12+
},
13+
);
14+
expect(result).toMatchInlineSnapshot(`
15+
Array [
16+
Object {
17+
"column": 10,
18+
"endColumn": 19,
19+
"endLine": 1,
20+
"line": 1,
21+
"message": "Cannot import a package-private export 'someValue'",
22+
"messageId": "package",
23+
"nodeType": "ImportSpecifier",
24+
"ruleId": "import-access/jsdoc",
25+
"severity": 2,
26+
},
27+
]
28+
`);
29+
});
30+
31+
it("Importing from generated package is allowed with excludeSourcePatterns targeting the file path (relative path)", async () => {
32+
const result = await tester.lintFile(
33+
"src/exclude-patterns/generated-type-user.ts",
34+
{
35+
jsdoc: {
36+
defaultImportability: "package",
37+
excludeSourcePatterns: [
38+
// exclude the types file
39+
"src/__tests__/fixtures/project/src/exclude-patterns/types/**",
40+
],
41+
},
42+
},
43+
);
44+
expect(result).toMatchInlineSnapshot(`Array []`);
45+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { someValue } from "generated-package";
2+
3+
console.log(someValue);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module "generated-package" {
2+
export const someValue: string;
3+
}

src/core/checkSymbolmportability.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { minimatch } from "minimatch";
2+
import path from "path";
13
import { Program, Symbol } from "typescript";
24
import { assertNever } from "../utils/assertNever";
35
import { concatArrays } from "../utils/concatArrays";
@@ -29,6 +31,25 @@ export function checkSymbolImportability(
2931
return;
3032
}
3133

34+
// Get the actual file name of the exported declaration
35+
const exporterFilename = decl.getSourceFile().fileName;
36+
37+
// Check if moduleSpecifier or exporter file path matches any of the excludeSourcePatterns
38+
if (packageOptions.excludeSourcePatterns?.length) {
39+
for (const pattern of packageOptions.excludeSourcePatterns) {
40+
// Check actual file path
41+
// Get relative path from the project root
42+
const projectPath = program.getCurrentDirectory();
43+
const relativePath = path.relative(projectPath, exporterFilename);
44+
45+
// Check if the file path matches the pattern
46+
if (minimatch(relativePath, pattern, { dot: true })) {
47+
// Skip importability check for this source
48+
return;
49+
}
50+
}
51+
}
52+
3253
// If declaration is from external module, treat as importable
3354
if (program.isSourceFileFromExternalLibrary(decl.getSourceFile())) {
3455
return;

src/rules/jsdoc.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export type JSDocRuleOptions = {
3030
* the importability check.
3131
*/
3232
treatSelfReferenceAs: "internal" | "external";
33+
/**
34+
* Array of glob patterns for source paths to exclude from the importability check.
35+
* Useful for excluding generated files or auto-generated type definitions.
36+
*/
37+
excludeSourcePatterns?: string[];
3338
};
3439

3540
const jsdocRule: Omit<
@@ -70,6 +75,12 @@ const jsdocRule: Omit<
7075
type: "string",
7176
enum: ["external", "internal"],
7277
},
78+
excludeSourcePatterns: {
79+
type: "array",
80+
items: {
81+
type: "string",
82+
},
83+
},
7384
},
7485
additionalProperties: false,
7586
},
@@ -81,6 +92,7 @@ const jsdocRule: Omit<
8192
filenameLoophole: false,
8293
defaultImportability: "public",
8394
treatSelfReferenceAs: "external",
95+
excludeSourcePatterns: [],
8496
},
8597
],
8698
create(context) {
@@ -94,13 +106,15 @@ const jsdocRule: Omit<
94106
filenameLoophole,
95107
defaultImportability,
96108
treatSelfReferenceAs,
109+
excludeSourcePatterns,
97110
} = jsDocRuleDefaultOptions(options[0]);
98111

99112
const packageOptions: PackageOptions = {
100113
indexLoophole,
101114
filenameLoophole,
102115
defaultImportability,
103116
treatSelfReferenceAs,
117+
excludeSourcePatterns,
104118
};
105119

106120
return {
@@ -249,12 +263,14 @@ export function jsDocRuleDefaultOptions(
249263
filenameLoophole = false,
250264
defaultImportability = "public",
251265
treatSelfReferenceAs = "external",
266+
excludeSourcePatterns = [],
252267
} = options || {};
253268
return {
254269
indexLoophole,
255270
filenameLoophole,
256271
defaultImportability,
257272
treatSelfReferenceAs,
273+
excludeSourcePatterns,
258274
};
259275
}
260276

src/utils/isInPackage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type PackageOptions = {
55
readonly filenameLoophole: boolean;
66
readonly defaultImportability: "public" | "package" | "private";
77
readonly treatSelfReferenceAs: "internal" | "external";
8+
readonly excludeSourcePatterns?: readonly string[];
89
};
910

1011
// ../ or ../../ or ...

0 commit comments

Comments
 (0)