Skip to content

feat: add --exclude-relative flag for path-based file exclusion#6741

Open
danielroymoore wants to merge 5 commits intomainfrom
feat/exclude-relative-paths
Open

feat: add --exclude-relative flag for path-based file exclusion#6741
danielroymoore wants to merge 5 commits intomainfrom
feat/exclude-relative-paths

Conversation

@danielroymoore
Copy link
Copy Markdown
Contributor

@danielroymoore danielroymoore commented Apr 23, 2026

Pull Request Submission Checklist

  • Follows CONTRIBUTING guidelines
  • Commit messages are release-note ready, emphasizing what was changed, not how.
  • Includes detailed description of changes
  • Contains risk assessment (Low | Medium | High)
  • Links to automated tests covering new functionality
  • Includes manual testing instructions (if necessary)

What does this PR do?

Adds a new --exclude-relative CLI flag that accepts comma-separated relative paths for excluding specific files and directories during --all-projects and --yarn-workspaces scans. This complements the existing --exclude flag, which only matches basenames.

The problem

The existing --exclude flag matches by basename only (e.g. --exclude=package.json excludes all package.json files). In workspace-style monorepos, this makes it impossible to exclude a specific package without excluding every package that shares the same manifest filename:

my-app/
  package.json         ← root
  packages/
    api/package.json   ← want to exclude this one
    web/package.json   ← but not this one

--exclude=package.json excludes all three. There was no way to target just packages/api/package.json.

What changed

New flag: --exclude-relative

  • Accepts comma-separated relative paths (e.g. --exclude-relative=packages/api/package.json,packages/web/package.json)
  • Requires --all-projects or --yarn-workspaces (same constraint as --exclude)
  • Input validation rejects absolute paths and parent directory traversals (..)
  • Whitespace around commas is trimmed

File discovery (find-files.ts)

  • Added excludePaths to FindFilesConfig — an array of resolved absolute paths
  • New isExcludedPath() helper performs exact path matching, with case-insensitive comparison on Windows
  • Paths are resolved once and reused for both exclusion filtering and recursive traversal (avoids duplicate pathLib.resolve calls)

Plugin integration (get-deps-from-plugin.ts)

  • Parses options.excludeRelative into resolved absolute paths and passes them as excludePaths to find()
  • After the workspace plugin returns scanned projects, filters out any that match excluded paths — this catches projects discovered by workspace parsers (e.g. pnpm) that bypass the filesystem walk
  • Includes excludePaths in analytics data

Validation & errors

  • ExcludeRelativeFlagInvalidInputError with clear messaging about allowed input format
  • MissingOptionError when used without --all-projects or --yarn-workspaces
  • args.ts registers 'exclude-relative' for camelCase transformation
  • types.ts adds the option to Options and SupportedUserReachableFacingCliArgs

Tests

  • Unit tests (test/tap/find-files.test.ts): Verifies excludePaths excludes specific files by absolute path, doesn't affect same-basename files at other paths, and can exclude entire directories
  • Acceptance tests (test/jest/acceptance/cli-args.spec.ts): Validates error messages for missing --all-projects, absolute paths, and .. traversal
  • Acceptance tests (test/jest/acceptance/snyk-test/all-projects.spec.ts): End-to-end tests with pnpm workspace fixture confirming single path exclusion, multiple path exclusion, and that non-targeted package.json files are unaffected

Where should the reviewer start?

  1. src/cli/main.ts — validation logic for the new flag
  2. src/lib/find-files.ts — core exclusion in the file discovery engine
  3. src/lib/plugins/get-deps-from-plugin.ts — integration with the plugin system and post-scan filtering

How should this be manually tested?

In a workspace monorepo with multiple package.json files:

# Without --exclude-relative (all packages scanned)
snyk test --all-projects

# Exclude a specific workspace package
snyk test --all-projects --exclude-relative=packages/api/package.json

# Exclude multiple paths
snyk test --all-projects --exclude-relative=packages/api/package.json,packages/web/package.json

# Verify --exclude still works as before (basename-only)
snyk test --all-projects --exclude=package.json

# Validation: should error
snyk test --all-projects --exclude-relative=/absolute/path/file.json
snyk test --all-projects --exclude-relative=../escape/file.json
snyk test --exclude-relative=packages/api/package.json  # missing --all-projects

Risk assessment: Low

  • Purely additive — no changes to existing --exclude behaviour or any other flags
  • The new flag goes through the same validation and discovery pipeline as --exclude
  • All changes are gated behind the presence of --exclude-relative; when absent, zero code paths are affected

Companion PR

This is consumed by snyk/cli-extension-dep-graph#152, which replaces the path.Split workaround in the orchestrator with --exclude-relative for forwarding processed file exclusions to the legacy CLI.

Trade-offs & callouts

  1. --exclude is unchanged. Existing behaviour is fully preserved. Both flags can be used together — --exclude for basename patterns, --exclude-relative for specific paths.

  2. Post-scan filtering in get-deps-from-plugin.ts. Workspace parsers (e.g. pnpm) discover projects by reading workspace config files rather than walking the filesystem, so they bypass the find() exclusion. An additional filter on inspectRes.scannedProjects catches these. This is intentional and mirrors how --exclude already needs special handling for workspace-discovered projects.

  3. No glob support. The flag accepts literal relative paths only, not globs. This keeps the implementation simple and predictable. Glob support could be added later if needed.

  4. Windows path handling. isExcludedPath() uses case-insensitive comparison on win32 to match Windows filesystem semantics. On Unix, comparison is exact.

The existing --exclude flag only accepts basenames, which causes
collateral exclusion in workspace-style projects where multiple files
share the same name (e.g. package.json). This adds --exclude-relative
to allow comma-separated relative paths for precise per-file exclusion
in --all-projects and --yarn-workspaces scans.

Made-with: Cursor
@danielroymoore danielroymoore requested review from a team as code owners April 23, 2026 08:00
@snyk-io
Copy link
Copy Markdown

snyk-io Bot commented Apr 23, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@snyk-pr-review-bot

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

Warnings
⚠️

Since the CLI is unifying on a standard and improved tooling, we're starting to migrate old-style imports and exports to ES6 ones.
A file you've modified is using either module.exports or require(). If you can, please update them to ES6 import syntax and export syntax.
Files found:

  • src/cli/args.ts
  • src/cli/main.ts
⚠️ There are multiple commits on your branch, please squash them locally before merging!

Generated by 🚫 dangerJS against 6789d22

@snyk-pr-review-bot

This comment has been minimized.

- Resolve path once in findInDirectory instead of twice
- Add case-insensitive path comparison for Windows
- Trim whitespace from comma-separated exclude-relative paths
- Fix Prettier formatting
@snyk-pr-review-bot

This comment has been minimized.

Workspace processors (e.g. processPnpmWorkspaces) read
pnpm-workspace.yaml directly and discover all workspace members,
bypassing the --exclude-relative file-level exclusion applied during
find(). Apply excludePaths as a post-filter on scannedProjects so
projects matched by --exclude-relative are consistently removed.

Made-with: Cursor
@snyk-pr-review-bot

This comment has been minimized.

@snyk-pr-review-bot
Copy link
Copy Markdown

PR Reviewer Guide 🔍

🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Exclusion Bypass 🟠 [major]

The isExcludedPath function relies on exact string equality between resolved absolute paths. Because pathLib.resolve (used in findInDirectory) does not resolve symlinks, if the search path or the excluded path contains symlinks, the strings may not match even if they point to the same file. Using fs.realpathSync for normalization would ensure consistent matching.

function isExcludedPath(resolvedPath: string, excludePaths: string[]): boolean {
  if (excludePaths.length === 0) {
    return false;
  }
  if (process.platform === 'win32') {
    const lowerPath = resolvedPath.toLowerCase();
    return excludePaths.some((ep) => ep.toLowerCase() === lowerPath);
  }
  return excludePaths.includes(resolvedPath);
}
Filtering Logic Inconsistency 🟡 [minor]

The manual filter in getDepsFromPlugin resolves project.meta.targetFile against the current root. If a plugin returns a path that is relative to a subdirectory (common in some monorepo plugins), the pathLib.resolve(root, targetFile) will produce an incorrect absolute path, causing the excludePaths.includes check to fail and the project to not be excluded as requested.

const resolved = pathLib.resolve(root, targetFile);
return !excludePaths.includes(resolved);
📚 Repository Context Analyzed

This review considered 21 relevant code sections from 8 files (average relevance: 0.73)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant