Skip to content

Commit

Permalink
Add ignoreUnresolved feature (resolves #920)
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Jan 22, 2025
1 parent f1d1c84 commit 081a776
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 11 deletions.
5 changes: 5 additions & 0 deletions packages/docs/src/content/docs/features/compilers.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ In this case, you can manually add the `$app` path alias:
}
```

As a last resort, see [ignoredUnresolved][1] to ignore virtual import specifiers
from the report.

### CSS

Here's an example, minimal compiler for CSS files:
Expand Down Expand Up @@ -134,3 +137,5 @@ const config = {

export default config;
```

[1]: ../reference/configuration.md#ignoreunresolved
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,10 @@ The following options are available inside workspace configurations:
- [ignoreBinaries][4]
- [ignoreDependencies][5]
- [ignoreMembers][6]
- [includeEntryExports][7]
- [ignoreUnresolved][7]
- [includeEntryExports][8]

[Plugins][8] can be configured separately per workspace.
[Plugins][9] can be configured separately per workspace.

Use `--debug` for verbose output and see the workspaces Knip includes, their
configurations, enabled plugins, glob options and resolved files.
Expand All @@ -149,7 +150,7 @@ workspaces. For two reasons:

To lint the workspace in isolation, there are two options:

- Combine the `workspace` argument with [strict production mode][9].
- Combine the `workspace` argument with [strict production mode][10].
- Run Knip from inside the workspace directory.

[1]: ../overview/configuration.md#defaults
Expand All @@ -158,6 +159,7 @@ To lint the workspace in isolation, there are two options:
[4]: ../reference/configuration.md#ignorebinaries
[5]: ../reference/configuration.md#ignoredependencies
[6]: ../reference/configuration.md#ignoremembers
[7]: ../reference/configuration.md#includeentryexports
[8]: ../reference/configuration.md#plugins
[9]: ./production-mode.md#strict-mode
[7]: ../reference/configuration.md#ignoreunresolved
[8]: ../reference/configuration.md#includeentryexports
[9]: ../reference/configuration.md#plugins
[10]: ./production-mode.md#strict-mode
19 changes: 19 additions & 0 deletions packages/docs/src/content/docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@ allowed. Example:

Actual regular expressions can be used in dynamic configurations.

### `ignoreUnresolved`

Array of specifiers to exclude from the report. Regular expressions allowed.
Example:

```json title="knip.json"
{
"ignoreUnresolved": ["ignore-unresolved-import", "#virtual/.+"]
}
```

Actual regular expressions can be used in dynamic configurations:

```ts title="knip.ts"
export default {
ignoreUnresolved: [/^#/.+/],
};
```

### `ignoreWorkspaces`

Array of workspaces to ignore, globs allowed. Example:
Expand Down
3 changes: 3 additions & 0 deletions packages/knip/fixtures/ignore-unresolved/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'missing-module';
import '#/ignored-unresolved-module';
import './ignored-by-regex';
3 changes: 3 additions & 0 deletions packages/knip/fixtures/ignore-unresolved/knip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
ignoreUnresolved: ['#/ignored-unresolved-module', /ignored.*regex/, 'unused-ignore'],
};
4 changes: 4 additions & 0 deletions packages/knip/fixtures/ignore-unresolved/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "@fixtures/ignore-unresolved",
"type": "module"
}
10 changes: 8 additions & 2 deletions packages/knip/src/ConfigurationChief.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,14 +398,20 @@ export class ConfigurationChief {
const workspaceConfig = this.getWorkspaceConfig(workspaceName);
const ignoreBinaries = arrayify(workspaceConfig.ignoreBinaries);
const ignoreDependencies = arrayify(workspaceConfig.ignoreDependencies);
const ignoreUnresolved = arrayify(workspaceConfig.ignoreUnresolved);
if (workspaceName === ROOT_WORKSPACE_NAME) {
const { ignoreBinaries: rootIgnoreBinaries, ignoreDependencies: rootIgnoreDependencies } = this.rawConfig ?? {};
const {
ignoreBinaries: rootIgnoreBinaries,
ignoreDependencies: rootIgnoreDependencies,
ignoreUnresolved: rootIgnoreUnresolved,
} = this.rawConfig ?? {};
return {
ignoreBinaries: compact([...ignoreBinaries, ...(rootIgnoreBinaries ?? [])]),
ignoreDependencies: compact([...ignoreDependencies, ...(rootIgnoreDependencies ?? [])]),
ignoreUnresolved: compact([...ignoreUnresolved, ...(rootIgnoreUnresolved ?? [])]),
};
}
return { ignoreBinaries, ignoreDependencies };
return { ignoreBinaries, ignoreDependencies, ignoreUnresolved };
}

public getConfigForWorkspace(workspaceName: string, extensions?: string[]) {
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/ConfigurationValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const rootConfigurationSchema = z.object({
ignoreBinaries: stringOrRegexSchema.optional(),
ignoreDependencies: stringOrRegexSchema.optional(),
ignoreMembers: stringOrRegexSchema.optional(),
ignoreUnresolved: stringOrRegexSchema.optional(),
ignoreExportsUsedInFile: ignoreExportsUsedInFileSchema.optional(),
ignoreWorkspaces: z.array(z.string()).optional(),
includeEntryExports: z.boolean().optional(),
Expand All @@ -74,6 +75,7 @@ const baseWorkspaceConfigurationSchema = z.object({
ignoreBinaries: stringOrRegexSchema.optional(),
ignoreDependencies: stringOrRegexSchema.optional(),
ignoreMembers: stringOrRegexSchema.optional(),
ignoreUnresolved: stringOrRegexSchema.optional(),
includeEntryExports: z.boolean().optional(),
});

Expand Down
29 changes: 29 additions & 0 deletions packages/knip/src/DependencyDeputy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class DependencyDeputy {
manifest,
ignoreDependencies,
ignoreBinaries,
ignoreUnresolved,
}: {
name: string;
cwd: string;
Expand All @@ -74,6 +75,7 @@ export class DependencyDeputy {
manifest: PackageJson;
ignoreDependencies: (string | RegExp)[];
ignoreBinaries: (string | RegExp)[];
ignoreUnresolved: (string | RegExp)[];
}) {
const dependencies = Object.keys(manifest.dependencies ?? {});
const peerDependencies = Object.keys(manifest.peerDependencies ?? {});
Expand Down Expand Up @@ -110,8 +112,10 @@ export class DependencyDeputy {
manifestPath,
ignoreDependencies: ignoreDependencies.map(toRegexOrString),
ignoreBinaries: ignoreBinaries.map(toRegexOrString),
ignoreUnresolved: ignoreUnresolved.map(toRegexOrString),
usedIgnoreDependencies: new Set<string | RegExp>(),
usedIgnoreBinaries: new Set<string | RegExp>(),
usedIgnoreUnresolved: new Set<string | RegExp>(),
dependencies,
devDependencies,
peerDependencies: new Set(peerDependencies),
Expand Down Expand Up @@ -385,13 +389,32 @@ export class DependencyDeputy {
}
}

handleIgnoredUnresolved(issues: Issues, counters: Counters) {
for (const key in issues.unresolved) {
const issueSet = issues.unresolved[key];
for (const issueKey in issueSet) {
const issue = issueSet[issueKey];
const manifest = this.getWorkspaceManifest(issue.workspace);
if (manifest) {
const ignoreItem = findMatch(manifest.ignoreUnresolved, issue.symbol);
if (ignoreItem) {
delete issueSet[issueKey];
counters.unresolved--;
manifest.usedIgnoreUnresolved.add(ignoreItem);
}
}
}
}
}

public removeIgnoredIssues({ issues, counters }: { issues: Issues; counters: Counters }) {
this.handleIgnoredDependencies(issues, counters, 'dependencies');
this.handleIgnoredDependencies(issues, counters, 'devDependencies');
this.handleIgnoredDependencies(issues, counters, 'optionalPeerDependencies');
this.handleIgnoredDependencies(issues, counters, 'unlisted');
this.handleIgnoredDependencies(issues, counters, 'unresolved');
this.handleIgnoredBinaries(issues, counters, 'binaries');
this.handleIgnoredUnresolved(issues, counters);
}

public getConfigurationHints() {
Expand All @@ -409,6 +432,12 @@ export class DependencyDeputy {
configurationHints.add({ workspaceName, identifier, type: 'ignoreBinaries' });
}
}

for (const identifier of manifest.ignoreUnresolved) {
if (!manifest.usedIgnoreUnresolved.has(identifier)) {
configurationHints.add({ workspaceName, identifier, type: 'ignoreUnresolved' });
}
}
}

return configurationHints;
Expand Down
3 changes: 1 addition & 2 deletions packages/knip/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ export const main = async (unresolvedConfiguration: CommandLineOptions) => {
const { name, dir, manifestPath } = workspace;
const manifest = chief.getManifestForWorkspace(name);
if (!manifest) continue;
const { ignoreBinaries, ignoreDependencies } = chief.getIgnores(name);
deputy.addWorkspace({ name, cwd, dir, manifestPath, manifest, ignoreBinaries, ignoreDependencies });
deputy.addWorkspace({ name, cwd, dir, manifestPath, manifest, ...chief.getIgnores(name) });
}

for (const workspace of workspaces) {
Expand Down
2 changes: 1 addition & 1 deletion packages/knip/src/types/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export type Rules = Record<IssueType, IssueSeverity>;
export type ConfigurationHints = Set<ConfigurationHint>;

export type ConfigurationHint = {
type: 'ignoreBinaries' | 'ignoreDependencies' | 'ignoreWorkspaces';
type: 'ignoreBinaries' | 'ignoreDependencies' | 'ignoreUnresolved' | 'ignoreWorkspaces';
identifier: string | RegExp;
workspaceName?: string;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/knip/src/types/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ type WorkspaceManifest = {
allDependencies: DependencySet;
ignoreDependencies: (string | RegExp)[];
ignoreBinaries: (string | RegExp)[];
ignoreUnresolved: (string | RegExp)[];
usedIgnoreDependencies: Set<string | RegExp>;
usedIgnoreBinaries: Set<string | RegExp>;
usedIgnoreUnresolved: Set<string | RegExp>;
};

export type WorkspaceManifests = Map<string, WorkspaceManifest>;
Expand Down
29 changes: 29 additions & 0 deletions packages/knip/test/ignore-unresolved.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test } from 'bun:test';
import assert from 'node:assert/strict';
import { main } from '../src/index.js';
import { resolve } from '../src/util/path.js';
import baseArguments from './helpers/baseArguments.js';
import baseCounters from './helpers/baseCounters.js';

const cwd = resolve('fixtures/ignore-unresolved');

test('Respect ignored unresolved imports, including regex, show config hints', async () => {
const { issues, counters, configurationHints } = await main({
...baseArguments,
cwd,
});

assert(issues.unlisted['index.ts']['missing-module']);

assert.deepEqual(counters, {
...baseCounters,
unlisted: 1,
processed: 2,
total: 2,
});

assert.deepEqual(
configurationHints,
new Set([{ type: 'ignoreUnresolved', workspaceName: '.', identifier: 'unused-ignore' }])
);
});

0 comments on commit 081a776

Please sign in to comment.