@crutchcrew/oxlint-plugin-import-access 1.0.0 β†’ 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # oxlint-plugin-import-access πŸ“¦πŸ”’
1
+ # @crutchcrew/oxlint-plugin-import-access πŸ“¦πŸ”’
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@crutchcrew/oxlint-plugin-import-access)](https://www.npmjs.com/package/@crutchcrew/oxlint-plugin-import-access)
4
4
  [![coverage](https://img.shields.io/endpoint?url=https://crutchcrew.github.io/oxlint-plugin-import-access/badges/coverage.json)](https://github.com/crutchcrew/oxlint-plugin-import-access/actions)
5
5
  [![provenance](https://img.shields.io/badge/provenance-verified-brightgreen)](https://www.npmjs.com/package/@crutchcrew/oxlint-plugin-import-access)
6
6
  [![license](https://img.shields.io/npm/l/@crutchcrew/oxlint-plugin-import-access)](./LICENSE)
7
7
 
8
- JSDoc import access for TypeScript, enforced by [oxlint](https://oxc.rs/docs/guide/usage/linter). Only `@public` exports can be imported from anywhere, and an export without an access tag is `@package`: only files in its own directory can import it. Tag an export `@public` to share it with the whole project, or `@private` so no other file can import it. To make untagged exports `@public` or `@private` instead, set [`defaultImportability`](#options).
8
+ JSDoc import access for TypeScript, enforced by [oxlint](https://oxc.rs/docs/guide/usage/linter). Only `@public` exports can be imported from anywhere, and an export without an access tag is `@package`: only files in its own package (its directory and subdirectories) can import it. Tag an export `@public` to share it with the whole project, or `@private` so no other file can import it. To make untagged exports `@public` or `@private` instead, set [`defaultImportability`](#options).
9
9
 
10
10
  A native oxlint JS plugin port of [eslint-plugin-import-access](https://github.com/uhyo/eslint-plugin-import-access) that needs **no type checker**: no `typescript` dependency, no `tsconfig.json` project loading, no typed linting setup.
11
11
 
@@ -38,7 +38,7 @@ This plugin adds a directory-level layer between a file and the whole project: a
38
38
  | **Checks unsaved edits in the editor** | βœ… | βœ… | ❌ skipped until saved |
39
39
  | **Same results as the original rule** | βœ… verifiedΒΉ | – | βœ… |
40
40
 
41
- ΒΉ With `defaultImportability: "public"`, which makes exports without an access tag `@public` like in the original. This plugin's default is `"package"`.
41
+ ΒΉ With `defaultImportability: "public"`, which makes exports without an access tag `@public` like in the original. This plugin's default is `"package"`. This plugin also checks the members used through namespace imports (`import * as foo`), which the original doesn't check.
42
42
 
43
43
  ## Install
44
44
 
@@ -110,11 +110,11 @@ The original rule asks the TypeScript type checker which declaration an import r
110
110
 
111
111
  1. resolve the import like TypeScript (`tsconfig` `paths`, `package.json` `exports`, `.js` β†’ `.ts`) with [oxc-resolver](https://github.com/oxc-project/oxc-resolver),
112
112
  2. skip builtins and anything reached through `node_modules`,
113
- 3. read the target file's exports and JSDoc with [oxc-parser](https://github.com/oxc-project/oxc/tree/main/napi/parser) β€” module record and comments only, no syntax tree β€” and cache them,
113
+ 3. read the target file's exports and JSDoc with [oxc-parser](https://github.com/oxc-project/oxc/tree/main/napi/parser) β€” the module record and comments; the syntax tree only for the few files with decorators, `export =`, re-exported imports or JSDoc inside variable statements β€” and cache them,
114
114
  4. follow `export *`, stop at direct re-exports like upstream, and read JSDoc with TypeScript's attachment and tag rules,
115
115
  5. check ambient `declare module "…"` and wildcard `declare module "*.svg"` declarations too.
116
116
 
117
- **Verified against the original:** the upstream test suite passes unchanged (same lines, columns, messages), and a differential test compares this plugin with upstream's TypeScript-based rule on every fixture file and option combination, including new fixtures for re-export chains, overloads, enums, `export =`, wildcard modules and JSDoc placement. The [known differences](./docs/rule-jsdoc.md#differences-from-upstream) are edge cases such as module augmentation across files.
117
+ **Verified against the original:** the upstream test suite passes unchanged (same lines, columns, messages), and the port was compared with upstream's TypeScript-based rule on every fixture file and option combination, including new fixtures for re-export chains, overloads, enums, `export =`, wildcard modules and JSDoc placement, whose expectations in the test suite were generated by the original rule. The [known differences](./docs/rule-jsdoc.md#differences-from-upstream) are edge cases such as module augmentation across files.
118
118
 
119
119
  ## Performance
120
120
 
@@ -39,7 +39,8 @@ export declare class ImportabilityChecker {
39
39
  constructor(watching?: boolean);
40
40
  /**
41
41
  * Start checking `fileName`. In a long-running host, everything read for earlier files is
42
- * re-validated: exporter files are stat-ed again and specifiers resolved again.
42
+ * re-validated: exporter files are stat-ed again, specifiers resolved again and package names
43
+ * looked up again.
43
44
  */
44
45
  nextFile(fileName: string): void;
45
46
  clear(): void;
@@ -52,6 +53,11 @@ export declare class ImportabilityPolicy {
52
53
  #private;
53
54
  readonly options: PackageOptions;
54
55
  constructor(options: PackageOptions);
56
+ /**
57
+ * Whether some export of `specifier` could be reported. Cheap way to skip namespace imports
58
+ * of external modules and of modules whose exports are all public.
59
+ */
60
+ mayReport(checker: ImportabilityChecker, importerFileName: string, specifier: string): boolean;
55
61
  /**
56
62
  * Check importing `name` (the export name, `default` for default imports) via `specifier`.
57
63
  */
package/dist/index.js CHANGED
@@ -737,6 +737,9 @@ function lookupPackageName(file) {
737
737
  }
738
738
  return result;
739
739
  }
740
+ function clearPackageNameCache() {
741
+ packageNameCache.clear();
742
+ }
740
743
 
741
744
  // src/core/resolveModule.ts
742
745
  import { realpathSync } from "node:fs";
@@ -903,6 +906,7 @@ class ImportabilityChecker {
903
906
  }
904
907
  this.exportTables.nextGeneration();
905
908
  this.resolver.clear();
909
+ clearPackageNameCache();
906
910
  this.#found = new WeakMap;
907
911
  }
908
912
  clear() {
@@ -910,6 +914,7 @@ class ImportabilityChecker {
910
914
  this.resolver.clear();
911
915
  this.exportTables.clear();
912
916
  this.ambientModules.clear();
917
+ clearPackageNameCache();
913
918
  }
914
919
  }
915
920
 
@@ -936,10 +941,17 @@ class ImportabilityPolicy {
936
941
  const packageName = lookupPackageName(importerFileName);
937
942
  return packageName !== null && (specifier === packageName || specifier.startsWith(`${packageName}/`));
938
943
  }
939
- check(checker, importerFileName, specifier, name) {
944
+ #targetModule(checker, importerFileName, specifier) {
940
945
  const { defaultImportability, projectDirectory } = this.options;
941
946
  const assumeAllPublic = defaultImportability === "public";
942
- const module = checker.targetModule(importerFileName, specifier, projectDirectory, assumeAllPublic);
947
+ return checker.targetModule(importerFileName, specifier, projectDirectory, assumeAllPublic);
948
+ }
949
+ mayReport(checker, importerFileName, specifier) {
950
+ return this.#targetModule(checker, importerFileName, specifier)?.names !== undefined;
951
+ }
952
+ check(checker, importerFileName, specifier, name) {
953
+ const { defaultImportability } = this.options;
954
+ const module = this.#targetModule(checker, importerFileName, specifier);
943
955
  const found = module && checker.findExport(module, name);
944
956
  if (found === undefined || !found.entries[0].checked) {
945
957
  return;
@@ -998,6 +1010,21 @@ function realFileName(fileName) {
998
1010
  function nameOf(node) {
999
1011
  return node.type === "Literal" ? node.value : node.name;
1000
1012
  }
1013
+ function accessedMember(identifier) {
1014
+ const parent = identifier.parent;
1015
+ if (parent.type === "MemberExpression" && parent.object === identifier) {
1016
+ const { property } = parent;
1017
+ if (property.type === "Identifier" && !parent.computed) {
1018
+ return { node: property, name: property.name };
1019
+ }
1020
+ if (property.type === "Literal" && typeof property.value === "string") {
1021
+ return { node: property, name: property.value };
1022
+ }
1023
+ } else if (parent.type === "TSQualifiedName") {
1024
+ return { node: parent.right, name: parent.right.name };
1025
+ }
1026
+ return;
1027
+ }
1001
1028
  var rule = {
1002
1029
  meta: {
1003
1030
  type: "problem",
@@ -1055,6 +1082,20 @@ var rule = {
1055
1082
  ImportDefaultSpecifier(node) {
1056
1083
  check(node, node.parent.source.value, "default", false);
1057
1084
  },
1085
+ ImportNamespaceSpecifier(node) {
1086
+ const specifier = node.parent.source.value;
1087
+ if (!policy.mayReport(checker, importer, specifier)) {
1088
+ return;
1089
+ }
1090
+ for (const variable of context.sourceCode.getDeclaredVariables(node)) {
1091
+ for (const { identifier } of variable.references) {
1092
+ const member = accessedMember(identifier);
1093
+ if (member !== undefined) {
1094
+ check(member.node, specifier, member.name, false);
1095
+ }
1096
+ }
1097
+ }
1098
+ },
1058
1099
  ExportSpecifier(node) {
1059
1100
  const { source } = node.parent;
1060
1101
  if (source !== null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crutchcrew/oxlint-plugin-import-access",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Oxlint JS plugin for import access via JSDoc @public, @package and @private, without a type checker",
5
5
  "keywords": [
6
6
  "import-access",