From c8a5e27e0e02c8efb0a78d13fdf38aee4eb3d220 Mon Sep 17 00:00:00 2001 From: Gaic4o <52266597+Gaic4o@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:34:57 +0900 Subject: [PATCH 1/2] feat: add the no-wildcard-exports rule --- .changeset/no-wildcard-exports.md | 5 + README.md | 1 + .../src/_language-tools/index.ts | 85 ++++- .../src/_language-tools/typescript.spec.ts | 28 +- packages/steiger-plugin-fsd/src/index.ts | 3 +- .../src/no-wildcard-exports/README.md | 53 +++ .../src/no-wildcard-exports/index.spec.ts | 337 ++++++++++++++++++ .../src/no-wildcard-exports/index.ts | 37 ++ .../src/no-wildcard-exports/real-fs.spec.ts | 64 ++++ packages/steiger/README.md | 1 + 10 files changed, 605 insertions(+), 9 deletions(-) create mode 100644 .changeset/no-wildcard-exports.md create mode 100644 packages/steiger-plugin-fsd/src/no-wildcard-exports/README.md create mode 100644 packages/steiger-plugin-fsd/src/no-wildcard-exports/index.spec.ts create mode 100644 packages/steiger-plugin-fsd/src/no-wildcard-exports/index.ts create mode 100644 packages/steiger-plugin-fsd/src/no-wildcard-exports/real-fs.spec.ts diff --git a/.changeset/no-wildcard-exports.md b/.changeset/no-wildcard-exports.md new file mode 100644 index 00000000..cd28904c --- /dev/null +++ b/.changeset/no-wildcard-exports.md @@ -0,0 +1,5 @@ +--- +'@feature-sliced/steiger-plugin': minor +--- + +add the `no-wildcard-exports` rule, which forbids `export * from` in public APIs but allows `export * as ns from`. It is disabled by default. diff --git a/README.md b/README.md index f58e6101..dc077675 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Currently, Steiger is not extendable with more rules, though that will change in fsd/typo-in-layer-name Ensure that all layers are named without any typos. fsd/no-processes Discourage the use of the deprecated Processes layer. fsd/import-locality [disabled] Require that imports from the same slice be relative and imports from one slice to another be absolute. + fsd/no-wildcard-exports [disabled] Forbid wildcard re-exports (export * from) in public APIs. diff --git a/packages/steiger-plugin-fsd/src/_language-tools/index.ts b/packages/steiger-plugin-fsd/src/_language-tools/index.ts index ffc0a5ce..11091178 100644 --- a/packages/steiger-plugin-fsd/src/_language-tools/index.ts +++ b/packages/steiger-plugin-fsd/src/_language-tools/index.ts @@ -44,6 +44,8 @@ interface Extractor { language: Language injections: Array<{ query: Query; lang: string }> queries: Array<{ query: Query; type: 'static' | 'dynamic' }> + /** Matches `export * from` statements, capturing the whole statement as `@statement` and the module specifier as `@path`. */ + wildcardExportQuery?: Query } const extractors: Array = [ @@ -90,6 +92,9 @@ const extractors: Array = [ type: 'dynamic', }, ], + // Deliberately does not match `export * as ns from`: a namespace re-export adds one name to the + // module's exports rather than an unknown number of them. + wildcardExportQuery: new Query(tsx, '(export_statement "*" source: (string (string_fragment) @path)) @statement'), injections: [], }, { @@ -170,6 +175,46 @@ function processExtractor(extractor: Extractor, tree: Tree): Dependency[] { return result } +function processWildcardExports(extractor: Extractor, tree: Tree): WildcardExport[] { + if (extractor.wildcardExportQuery === undefined) return [] + + const result: WildcardExport[] = [] + + for (const match of extractor.wildcardExportQuery.matches(tree.rootNode)) { + const pathCapture = match.captures.find((capture) => capture.name === 'path') + const statementCapture = match.captures.find((capture) => capture.name === 'statement') + if (pathCapture === undefined || statementCapture === undefined) continue + + result.push({ + path: pathCapture.node.text, + start: { + line: statementCapture.node.startPosition.row + 1, + column: statementCapture.node.startPosition.column + 1, + }, + end: { + line: statementCapture.node.endPosition.row + 1, + column: statementCapture.node.endPosition.column + 1, + }, + }) + } + + return result +} + +interface WildcardExport { + /** The module specifier that the names are re-exported from. */ + path: string + // all indexes are 1-based, and they span the whole `export * from` statement + start: { + line: number + column: number + } + end: { + line: number + column: number + } +} + interface Dependency { path: string builtIn: boolean @@ -185,14 +230,16 @@ interface Dependency { } } -const cache = createFSCache() - -function extractAllDependencies(path: string): Dependency[] { +/** + * Parse a source file and run `process` on its syntax tree, as well as on the syntax trees of the + * languages injected into it (for example, the `