Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/ripe-places-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@konsistent/convention": patch
"konsistent": patch
---

fix(konsistent): fix `importFrom` predicate to distinguish between exact vs sub match imports
6 changes: 3 additions & 3 deletions docs/guides/fixing-violations.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,14 @@ Search first: grep for `X` and case/suffix variants. Also look for object factor

#### `import` / `importTypes` / `importFrom`

Message: `Missing import "X" from "<module>"`, `Missing import type "X" from "<module>"`, or `Missing import from "<module>"`.
Message: `Missing import "X" from "<module>"`, `Missing import type "X" from "<module>"`, or `Missing import from "<specifier>"`.

Search first: confirm the export `X` exists at `<module>` for named-import rules, or that the target module/package is already used by sibling files for `importFrom`. If not, find where `X` (or its variants) is currently imported from.
Search first: confirm the export `X` exists at `<module>` for named-import rules, or that the target module specifier is already used by sibling files for `importFrom`. If not, find where `X` (or its variants) is currently imported from.

- **Trivial**:
- Export exists at the specified module path → add the import statement.
- Symbol is currently imported from a different path → change the import path.
- For `importFrom`, a sibling file imports from the same package/path for the same role → add the analogous import.
- For `importFrom`, a sibling file imports from the same exact specifier, or from a subpath covered by a configured `/*` wildcard, for the same role → add the analogous import.
- **Non-trivial**:
- Export does not exist at the target path *and* the symbol does not exist anywhere. Usually cascades from a separate violation — fix that first; this one may resolve automatically.
- For `importFrom`, no sibling establishes why the dependency should exist. Adding a new dependency may be a design decision.
Expand Down
10 changes: 6 additions & 4 deletions docs/reference/predicates.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,19 +328,21 @@ Bare-string form (`"import": ["useState"]`) checks the binding regardless of sou

### `importFrom`

Assert that the file has at least one import statement from a specific module path or package.
Assert that the file has at least one import statement from a specific module specifier.

```json
"must": { "importFrom": "react" }
```

```json
"must": { "importFrom": "./setup" }
"must": { "importFrom": ["./setup", "package/*"] }
```

The value is a string. Relative and absolute path-like specifiers are matched exactly, so `"./setup"` only matches `import "./setup"`.
The value is a string or an array of strings. When an array is used in `must`, every entry must be satisfied.

Package roots match the package itself and its subpaths. For example, `"react"` matches imports from `"react"` and `"react/jsx-runtime"`, and `"@scope/pkg"` matches `"@scope/pkg"` and `"@scope/pkg/subpath"`. Similarly named packages such as `"react-dom"` or `"@scope/pkg-extra"` do not match.
By default, entries match exactly. For example, `"package"` only matches `import ... from "package"` and does not match `import ... from "package/v4"`.

Use a trailing `/*` to match subpaths under a prefix. For example, `"package/*"` matches `"package/v4"`, and `"@scope/pkg/*"` matches `"@scope/pkg/subpath"`. The wildcard does not match the root itself, so `"package/*"` does not match `"package"`.

Both value imports and type-only imports count because this predicate checks import statements by source. Side-effect imports such as `import "./setup"` also count.

Expand Down
7 changes: 6 additions & 1 deletion e2e/fixtures/import-from-broken/konsistent.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
{
"name": "package-imports",
"paths": "src/package.ts",
"must": { "importFrom": "@scope/pkg" }
"must": { "importFrom": ["@scope/pkg/*", "react"] }
},
{
"name": "no-react-imports",
"paths": "src/no-react.ts",
"mustNot": { "importFrom": "react" }
},
{
"name": "no-package-subpath-imports",
"paths": "src/no-package-subpath.ts",
"mustNot": { "importFrom": "package/*" }
}
]
}
3 changes: 3 additions & 0 deletions e2e/fixtures/import-from-broken/src/no-package-subpath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { value } from "package/v4";

export const packageSubpath = value;
7 changes: 6 additions & 1 deletion e2e/fixtures/import-from/konsistent.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
{
"name": "package-imports",
"paths": "src/package.ts",
"must": { "importFrom": "@scope/pkg" }
"must": { "importFrom": ["@scope/pkg/*", "react"] }
},
{
"name": "no-react-imports",
"paths": "src/no-react.ts",
"mustNot": { "importFrom": "react" }
},
{
"name": "no-package-root-imports",
"paths": "src/no-package-root.ts",
"mustNot": { "importFrom": "package" }
}
]
}
3 changes: 3 additions & 0 deletions e2e/fixtures/import-from/src/no-package-root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { value } from "package/v4";

export const packageSubpath = value;
2 changes: 2 additions & 0 deletions e2e/fixtures/import-from/src/package.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Tool } from "@scope/pkg/tools";
import React from "react";

export type PackageTool = Tool;
export const packageReact = React;
4 changes: 3 additions & 1 deletion e2e/new-predicates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,10 @@ describe("import-from-broken fixture", () => {
const error = err as { stdout: string; code: number; status: number };
expect(error.code ?? error.status).toBe(1);
expect(error.stdout).toContain('Missing import from "./helper"');
expect(error.stdout).toContain('Missing import from "@scope/pkg"');
expect(error.stdout).toContain('Missing import from "@scope/pkg/*"');
expect(error.stdout).toContain('Missing import from "react"');
expect(error.stdout).toContain('Forbidden import from "react"');
expect(error.stdout).toContain('Forbidden import from "package/*"');
}
});
});
48 changes: 44 additions & 4 deletions packages/convention/reusable-convention-package.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,17 @@
}
},
"importFrom": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"importTypes": {
"type": "array",
Expand Down Expand Up @@ -974,7 +984,17 @@
}
},
"importFrom": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"importTypes": {
"type": "array",
Expand Down Expand Up @@ -1526,7 +1546,17 @@
}
},
"importFrom": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"importTypes": {
"type": "array",
Expand Down Expand Up @@ -1997,7 +2027,17 @@
}
},
"importFrom": {
"type": "string"
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"importTypes": {
"type": "array",
Expand Down
2 changes: 1 addition & 1 deletion packages/convention/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("ReusableConventionV1Schema", () => {
},
],
useDeclarationOrder: ["localValue", "createLocal"],
importFrom: "react",
importFrom: ["react", "zod/*"],
importFromCurrentDir: true,
importFromParents: false,
importFromExternals: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/convention/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const MustPredicatesV1Schema = z.strictObject({
.array(z.union([z.string(), ClassDefinitionV1Schema]))
.optional(),
import: z.array(z.union([z.string(), ImportDefinitionV1Schema])).optional(),
importFrom: z.string().optional(),
importFrom: z.union([z.string(), z.array(z.string())]).optional(),
importTypes: z
.array(z.union([z.string(), ImportDefinitionV1Schema]))
.optional(),
Expand Down
Loading