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 `