From 929ee9098a9548a6e24621e0b4ad9dce2fcd3e91 Mon Sep 17 00:00:00 2001 From: neema-beglou-snyk Date: Tue, 19 May 2026 14:10:09 +0100 Subject: [PATCH 1/4] feat: consolidate print-graph flags [CSENG-190] --- .../cmd/cliv2/behavior/maperrortoexitcode.go | 8 +- cliv2/internal/cliv2/cliv2.go | 5 + cliv2/internal/cliv2/cliv2_test.go | 4 + src/cli/commands/monitor/index.ts | 3 +- src/lib/ecosystems/test.ts | 7 +- src/lib/monitor/index.ts | 3 +- src/lib/plugins/get-deps-from-plugin.ts | 8 +- src/lib/plugins/get-multi-plugin-result.ts | 3 +- src/lib/snyk-test/common.ts | 81 ++++++++--- src/lib/snyk-test/index.js | 9 +- src/lib/snyk-test/run-test.ts | 39 +++--- src/lib/types.ts | 4 + .../unit/print-graph-flag-resolution.spec.ts | 126 ++++++++++++++++++ 13 files changed, 240 insertions(+), 60 deletions(-) create mode 100644 test/jest/unit/print-graph-flag-resolution.spec.ts diff --git a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go index 96f0b446ed..488ea7056d 100644 --- a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go +++ b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go @@ -2,6 +2,7 @@ package behavior import ( "github.com/snyk/error-catalog-golang-public/aibom" + "github.com/snyk/error-catalog-golang-public/cli" "github.com/snyk/error-catalog-golang-public/code" "github.com/snyk/error-catalog-golang-public/snyk" "github.com/snyk/error-catalog-golang-public/snyk_errors" @@ -14,9 +15,10 @@ var MapErrorCatalogToExitCode func(err *snyk_errors.Error, defaultValue int) int // mapErrorToExitCode maps error catalog errors to exit codes. Please extend the switch statement if new error codes need to be mapped. func mapErrorToExitCode(err *snyk_errors.Error, defaultValue int) int { var errorCatalogToExitCodeMap = map[string]int{ - code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL, + code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + cli.NewNoSupportedFilesFoundError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL, // Add new mappings here } diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index cb62d2ebce..3569eda66e 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -31,6 +31,7 @@ import ( cli_errors "github.com/snyk/cli/cliv2/internal/errors" + "github.com/snyk/cli/cliv2/cmd/cliv2/behavior" "github.com/snyk/cli/cliv2/internal/constants" debug_utils "github.com/snyk/cli/cliv2/internal/debug" "github.com/snyk/cli/cliv2/internal/embedded" @@ -584,6 +585,7 @@ func DeriveExitCode(err error) int { if err != nil { var exitError *exec.ExitError var errorWithExitCode *cli_errors.ErrorWithExitCode + var snykErr snyk_errors.Error if errors.As(err, &exitError) { returnCode = exitError.ExitCode() @@ -593,6 +595,8 @@ func DeriveExitCode(err error) int { } } else if errors.As(err, &errorWithExitCode) { returnCode = errorWithExitCode.ExitCode + } else if errors.As(err, &snykErr) { + returnCode = behavior.MapErrorCatalogToExitCode(&snykErr, constants.SNYK_EXIT_CODE_ERROR) } else { // got an error but it's not an ExitError returnCode = constants.SNYK_EXIT_CODE_ERROR @@ -601,6 +605,7 @@ func DeriveExitCode(err error) int { return returnCode } + func (e EnvironmentWarning) Error() string { return e.message } diff --git a/cliv2/internal/cliv2/cliv2_test.go b/cliv2/internal/cliv2/cliv2_test.go index 54da22d7ae..c7d6bdda43 100644 --- a/cliv2/internal/cliv2/cliv2_test.go +++ b/cliv2/internal/cliv2/cliv2_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/snyk/error-catalog-golang-public/code" "github.com/snyk/error-catalog-golang-public/snyk_errors" "github.com/snyk/go-application-framework/pkg/app" "github.com/snyk/go-application-framework/pkg/configuration" @@ -654,6 +655,9 @@ func TestDeriveExitCode(t *testing.T) { {name: "no error", err: nil, expected: constants.SNYK_EXIT_CODE_OK}, {name: "error with exit code", err: &cli_errors.ErrorWithExitCode{ExitCode: 42}, expected: 42}, {name: "other error", err: errors.New("some other error"), expected: constants.SNYK_EXIT_CODE_ERROR}, + {name: "snyk_errors.Error with mapped code", err: snyk_errors.Error{ErrorCode: code.NewUnsupportedProjectError("").ErrorCode}, expected: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS}, + {name: "snyk_errors.Error with unmapped code", err: snyk_errors.Error{ErrorCode: "SNYK-UNKNOWN-9999"}, expected: constants.SNYK_EXIT_CODE_ERROR}, + {name: "wrapped snyk_errors.Error", err: fmt.Errorf("wrap: %w", snyk_errors.Error{ErrorCode: code.NewUnsupportedProjectError("").ErrorCode}), expected: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS}, } for _, tc := range tests { diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 70e6fbbc35..effdcf9659 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -219,8 +219,7 @@ export default async function monitor(...args0: MethodArgs): Promise { const verboseEnabled = args.includes('-Dverbose') || args.includes('-Dverbose=true') || - !!options['print-graph'] || - !!options['print-output-jsonl-with-errors']; + !!options['print-graph']; if (verboseEnabled) { enableMavenDverboseExhaustiveDeps = (await hasFeatureFlag( MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, diff --git a/src/lib/ecosystems/test.ts b/src/lib/ecosystems/test.ts index 3cff71bda6..2029e9cc4a 100644 --- a/src/lib/ecosystems/test.ts +++ b/src/lib/ecosystems/test.ts @@ -13,7 +13,8 @@ import { assembleQueryString, printDepGraph, printDepGraphJsonl, - shouldPrintDepGraph, + shouldPrintGraph, + isJsonl, } from '../snyk-test/common'; import { getAuthHeader } from '../api-token'; import { resolveAndTestFacts } from './resolve-test-facts'; @@ -57,7 +58,7 @@ export async function testEcosystem( if ( isUnmanagedEcosystem(ecosystem) && - (shouldPrintDepGraph(options) || options['print-output-jsonl-with-errors']) + shouldPrintGraph(options) ) { const [target] = paths; return printUnmanagedDepGraph(results, target, process.stdout, options); @@ -108,7 +109,7 @@ export async function printUnmanagedDepGraph( const [result] = await getUnmanagedDepGraph(results); const depGraph = convertDepGraph(result); - if (options['print-output-jsonl-with-errors']) { + if (isJsonl(options)) { await printDepGraphJsonl( depGraph, target, diff --git a/src/lib/monitor/index.ts b/src/lib/monitor/index.ts index f613b61cc8..80c80a3d12 100644 --- a/src/lib/monitor/index.ts +++ b/src/lib/monitor/index.ts @@ -317,7 +317,8 @@ export async function monitorDepGraph( analytics.add('targetBranch', target.branch); } - const pruneIsRequired = options.pruneRepeatedSubdependencies; + const pruneIsRequired = + options.pruneRepeatedSubdependencies || !!options['prune']; depGraph = await pruneGraph(depGraph, packageManager, pruneIsRequired); let callGraphPayload; diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index b977f1783e..ab2afcd294 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -17,6 +17,7 @@ import { detectPackageManagerFromFile, } from '../detect'; import * as analytics from '../analytics'; +import { shouldEmbedErrors } from '../snyk-test/common'; import { convertSingleResultToMultiCustom } from './convert-single-splugin-res-to-multi-custom'; import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-multi-custom'; import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser'; @@ -65,7 +66,7 @@ export async function getDepsFromPlugin( ); if (targetFiles.length === 0) { const error = NoSupportedManifestsFoundError([root]); - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { return { plugin: { name: 'custom-auto-detect' }, scannedProjects: [], @@ -138,7 +139,7 @@ export async function getDepsFromPlugin( } if (!options.docker && !(options.file || options.packageManager)) { const error = NoSupportedManifestsFoundError([root]); - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { return { plugin: { name: 'custom-auto-detect' }, scannedProjects: [], @@ -149,6 +150,7 @@ export async function getDepsFromPlugin( errMessage: error.userMessage, }, ], + } as MultiProjectResultCustom; } throw error; @@ -158,7 +160,7 @@ export async function getDepsFromPlugin( try { inspectRes = await getSinglePluginResult(root, options, '', featureFlags); } catch (error) { - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { const errMessage = error?.message ?? 'Something went wrong getting dependencies'; debug( diff --git a/src/lib/plugins/get-multi-plugin-result.ts b/src/lib/plugins/get-multi-plugin-result.ts index c3f124b117..4bb0665600 100644 --- a/src/lib/plugins/get-multi-plugin-result.ts +++ b/src/lib/plugins/get-multi-plugin-result.ts @@ -18,6 +18,7 @@ import { convertMultiResultToMultiCustom } from './convert-multi-plugin-res-to-m import { PluginMetadata } from '@snyk/cli-interface/legacy/plugin'; import { CallGraph } from '@snyk/cli-interface/legacy/common'; import { errorMessageWithRetry, FailedToRunTestError } from '../errors'; +import { shouldEmbedErrors } from '../snyk-test/common'; import { processYarnWorkspaces } from './nodejs-plugin/yarn-workspaces-parser'; import { processNpmWorkspaces } from './nodejs-plugin/npm-workspaces-parser'; import { processPnpmWorkspaces } from 'snyk-nodejs-plugin'; @@ -203,7 +204,7 @@ export async function getMultiPluginResult( } if (!allResults.length) { - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { return { plugin: { name: 'custom-auto-detect', diff --git a/src/lib/snyk-test/common.ts b/src/lib/snyk-test/common.ts index 67db2866f7..55153a3594 100644 --- a/src/lib/snyk-test/common.ts +++ b/src/lib/snyk-test/common.ts @@ -11,6 +11,7 @@ import { ContainerTarget, GitTarget } from '../project-metadata/types'; import { CLI, ProblemError } from '@snyk/error-catalog-nodejs-public'; import { CustomError } from '../errors'; import { FailedProjectScanError } from '../plugins/get-multi-plugin-result'; +import * as analytics from '../analytics'; /** * Determines workspace information from the plugin name and scanned project metadata. @@ -156,8 +157,67 @@ export async function printDepGraph( }); } -export function shouldPrintDepGraph(opts: Options): boolean { - return opts['print-graph'] && !opts['print-deps']; +// PHASE 2: --jsonl will be removed once all consumers use the dep-graph router +// directly. At that point, JSONL will be the only print-graph output format. +export function isJsonl(opts: Options): boolean { + return !!opts['jsonl']; +} + +export function shouldEmbedErrors(opts: Options): boolean { + return !!opts['embed-errors']; +} + +export function shouldPrintGraph(opts: Options): boolean { + return !opts['print-deps'] && ( + !!opts['print-graph'] || + !!opts['print-effective-graph'] || + !!opts['print-effective-graph-with-errors'] || + !!opts['print-output-jsonl-with-errors'] + ); +} + +// DEPRECATION: The legacy flag mappings below exist for backward compatibility. +// Once analytics confirm no usage of the old flags, remove the legacyMappings +// table and the deprecation warnings. +export function mapLegacyGraphFlags(opts: Options): void { + // --prune implies --jsonl (pruned output is always JSONL) + if (opts['prune']) { + opts['jsonl'] = true; + opts['print-graph'] = true; + } + + // New-style --jsonl or --prune: always embed errors. + // Consumers of the new model are expected to handle embedded errors. + if (opts['jsonl']) { + opts['print-graph'] = true; + opts['embed-errors'] = true; + return; + } + + const legacyMappings: Array<{ flag: keyof Options; prune: boolean; embedErrors: boolean }> = [ + { flag: 'print-effective-graph', prune: true, embedErrors: false }, + { flag: 'print-effective-graph-with-errors', prune: true, embedErrors: true }, + { flag: 'print-output-jsonl-with-errors', prune: false, embedErrors: true }, + ]; + + for (const { flag, prune, embedErrors } of legacyMappings) { + if (opts[flag]) { + const replacement = prune ? '--print-graph --prune' : '--print-graph --jsonl'; + process.stderr.write( + `WARNING: --${flag} is deprecated. Use ${replacement} instead.\n`, + ); + analytics.add('deprecatedLegacyDepGraphFlag', flag); + opts['print-graph'] = true; + opts['jsonl'] = true; + if (prune) { + opts['prune'] = true; + } + if (embedErrors) { + opts['embed-errors'] = true; + } + return; + } + } } /** @@ -225,23 +285,6 @@ export async function printDepGraphError( }); } -/** - * Checks if either --print-effective-graph or --print-effective-graph-with-errors is set. - */ -export function shouldPrintEffectiveDepGraph(opts: Options): boolean { - return ( - !!opts['print-effective-graph'] || - shouldPrintEffectiveDepGraphWithErrors(opts) - ); -} - -/** - * shouldPrintEffectiveDepGraphWithErrors checks if the --print-effective-graph-with-errors flag is set. - * This is used to determine if the effective dep-graph with errors should be printed. - */ -export function shouldPrintEffectiveDepGraphWithErrors(opts: Options): boolean { - return !!opts['print-effective-graph-with-errors']; -} /** * getOrCreateErrorCatalogError returns a ProblemError instance for consistent error catalog usage. diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index 03c98980fb..bb4b36fcf2 100644 --- a/src/lib/snyk-test/index.js +++ b/src/lib/snyk-test/index.js @@ -19,7 +19,7 @@ const { DISABLE_GO_PACKAGE_URLS_IN_CLI_FEATURE_FLAG, } = require('../package-managers'); const { getOrganizationID } = require('../organization'); -const { printDepGraphError } = require('./common'); +const { printDepGraphError, mapLegacyGraphFlags, shouldEmbedErrors } = require('./common'); const debug = require('debug')('snyk-test'); async function test(root, options, callback) { @@ -42,6 +42,8 @@ async function test(root, options, callback) { } async function executeTest(root, options) { + mapLegacyGraphFlags(options); + const includeGoStandardLibraryDeps = await hasFeatureFlagOrDefault( INCLUDE_GO_STANDARD_LIBRARY_DEPS_FEATURE_FLAG, options, @@ -57,8 +59,7 @@ async function executeTest(root, options) { const verboseEnabled = args.includes('-Dverbose') || args.includes('-Dverbose=true') || - !!options['print-graph'] || - !!options['print-output-jsonl-with-errors']; + (!!options['print-graph'] && !options['prune']); if (verboseEnabled) { enableMavenDverboseExhaustiveDeps = await hasFeatureFlag( MAVEN_DVERBOSE_EXHAUSTIVE_DEPS_FF, @@ -117,7 +118,7 @@ async function executeTest(root, options) { featureFlags, ); } catch (error) { - if (options['print-output-jsonl-with-errors']) { + if (shouldEmbedErrors(options)) { await printDepGraphError( root, { diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 89906cc347..0530282da2 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -44,9 +44,9 @@ import { printDepGraphJsonl, printDepGraphError, assembleQueryString, - shouldPrintDepGraph, - shouldPrintEffectiveDepGraph, - shouldPrintEffectiveDepGraphWithErrors, + shouldPrintGraph, + isJsonl, + shouldEmbedErrors, } from './common'; import config from '../config'; import * as analytics from '../analytics'; @@ -356,10 +356,10 @@ async function sendAndParseResults( }); } - if (ecosystem && shouldPrintDepGraph(options)) { + if (ecosystem && shouldPrintGraph(options)) { await spinner.clear(spinnerLbl)(); - if (options['print-output-jsonl-with-errors']) { - for (const { graph, targetFile, targetName } of depGraphs) { + for (const { graph, targetFile, targetName } of depGraphs) { + if (isJsonl(options)) { await printDepGraphJsonl( graph, targetFile || targetName, @@ -370,12 +370,7 @@ async function sendAndParseResults( undefined, process.stdout, ); - } - } else { - const depGraphsByTarget = new Map( - depGraphs.map(({ targetName, graph }) => [targetName, graph]), - ); - for (const [targetName, graph] of depGraphsByTarget) { + } else { await printDepGraph(graph, targetName, process.stdout); } } @@ -401,9 +396,7 @@ export async function runTest( // dependency graph artifacts for printing. if ( !options.docker && - (shouldPrintDepGraph(options) || - shouldPrintEffectiveDepGraph(options) || - options['print-output-jsonl-with-errors']) + shouldPrintGraph(options) ) { return []; } @@ -701,10 +694,7 @@ async function assembleLocalPayloads( failedResults, ); - if ( - shouldPrintEffectiveDepGraphWithErrors(options) || - options['print-output-jsonl-with-errors'] - ) { + if (shouldPrintGraph(options) && shouldEmbedErrors(options)) { for (const failed of failedResults) { await printDepGraphError(root, failed, process.stdout); } @@ -844,7 +834,7 @@ async function assembleLocalPayloads( ? (pkg as depGraphLib.DepGraph).rootPkg.name : (pkg as DepTree).name; - if (shouldPrintDepGraph(options)) { + if (shouldPrintGraph(options) && !options['prune']) { spinner.clear(spinnerLbl)(); let root: depGraphLib.DepGraph; if (scannedProject.depGraph) { @@ -857,7 +847,7 @@ async function assembleLocalPayloads( ); } - if (options['print-output-jsonl-with-errors']) { + if (isJsonl(options)) { await printDepGraphJsonl( root.toJSON(), targetFile || '', @@ -909,13 +899,14 @@ async function assembleLocalPayloads( }); } - const pruneIsRequired = options.pruneRepeatedSubdependencies; + const pruneIsRequired = + options.pruneRepeatedSubdependencies || !!options['prune']; - if (packageManager && !options['print-output-jsonl-with-errors']) { + if (packageManager && (!isJsonl(options) || options['prune'])) { depGraph = await pruneGraph(depGraph, packageManager, pruneIsRequired); } - if (shouldPrintEffectiveDepGraph(options)) { + if (shouldPrintGraph(options) && options['prune']) { spinner.clear(spinnerLbl)(); await printDepGraphJsonl( depGraph.toJSON(), diff --git a/src/lib/types.ts b/src/lib/types.ts index 948aeee47f..e5c1d922bc 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -68,9 +68,13 @@ export interface Options { 'print-deps'?: boolean; 'print-tree'?: boolean; 'print-dep-paths'?: boolean; + 'print-graph'?: boolean; 'print-effective-graph'?: boolean; 'print-effective-graph-with-errors'?: boolean; 'print-output-jsonl-with-errors'?: boolean; + 'prune'?: boolean; + 'jsonl'?: boolean; + 'embed-errors'?: boolean; 'remote-repo-url'?: string; criticality?: string; scanAllUnmanaged?: boolean; diff --git a/test/jest/unit/print-graph-flag-resolution.spec.ts b/test/jest/unit/print-graph-flag-resolution.spec.ts new file mode 100644 index 0000000000..2ab2a40c44 --- /dev/null +++ b/test/jest/unit/print-graph-flag-resolution.spec.ts @@ -0,0 +1,126 @@ +import { + mapLegacyGraphFlags, + shouldPrintGraph, + isJsonl, + shouldEmbedErrors, +} from '../../../src/lib/snyk-test/common'; +import { Options, TestOptions } from '../../../src/lib/types'; + +describe('print-graph flag resolution', () => { + let stderrSpy: jest.SpyInstance; + + const baseOptions: Options & TestOptions = { + path: '', + showVulnPaths: 'some', + }; + + beforeEach(() => { + stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); + }); + + afterEach(() => { + stderrSpy.mockRestore(); + }); + + it('bare --print-graph resolves to plaintext (no jsonl)', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(false); + expect(shouldEmbedErrors(opts)).toBe(false); + expect(opts['prune']).toBeFalsy(); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--print-graph --jsonl resolves to complete JSONL (no pruning)', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, jsonl: true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(opts['prune']).toBeFalsy(); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--print-graph --prune resolves to pruned JSONL', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, prune: true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(opts['prune']).toBe(true); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--prune alone implies --print-graph and --jsonl', () => { + const opts: Options & TestOptions = { ...baseOptions, prune: true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(opts['print-graph']).toBe(true); + }); + + it('--print-effective-graph maps to pruned JSONL but throws on errors', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-effective-graph': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(opts['prune']).toBe(true); + expect(opts['print-graph']).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(false); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('--print-effective-graph is deprecated'), + ); + }); + + it('--print-effective-graph-with-errors maps to pruned JSONL with embedded errors', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-effective-graph-with-errors': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(opts['prune']).toBe(true); + expect(opts['print-graph']).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('--print-effective-graph-with-errors is deprecated'), + ); + }); + + it('--print-output-jsonl-with-errors maps to unpruned JSONL with embedded errors', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-output-jsonl-with-errors': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(true); + expect(isJsonl(opts)).toBe(true); + expect(opts['prune']).toBeFalsy(); + expect(opts['print-graph']).toBe(true); + expect(shouldEmbedErrors(opts)).toBe(true); + expect(stderrSpy).toHaveBeenCalledWith( + expect.stringContaining('--print-output-jsonl-with-errors is deprecated'), + ); + }); + + it('no print flags resolves to no graph printing', () => { + const opts: Options & TestOptions = { ...baseOptions }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(false); + expect(isJsonl(opts)).toBe(false); + expect(shouldEmbedErrors(opts)).toBe(false); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it('--print-deps alongside --print-graph suppresses graph printing', () => { + const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, 'print-deps': true }; + mapLegacyGraphFlags(opts); + + expect(shouldPrintGraph(opts)).toBe(false); + }); +}); From 62af7e0f4941f30ff4750f9b43d10079724acb4c Mon Sep 17 00:00:00 2001 From: neema-beglou-snyk Date: Tue, 9 Jun 2026 15:22:23 +0100 Subject: [PATCH 2/4] test: expect print-effective-graph deprecation warning [CSENG-190] --- .../reachability-user-journey.spec.ts | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/test/jest/acceptance/snyk-test/reachability-user-journey.spec.ts b/test/jest/acceptance/snyk-test/reachability-user-journey.spec.ts index be377006a6..1479b72a0b 100644 --- a/test/jest/acceptance/snyk-test/reachability-user-journey.spec.ts +++ b/test/jest/acceptance/snyk-test/reachability-user-journey.spec.ts @@ -62,7 +62,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); expect(stdout).toContain('Test Summary'); expect(code).toBe(EXIT_CODES.VULNS_FOUND); @@ -81,7 +83,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); expect(stdout).toContain('Ignored: 1'); expect(code).toBe(EXIT_CODES.VULNS_FOUND); @@ -100,7 +104,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); expect(stdout).toContain('Ignored: 0'); expect(code).toBe(EXIT_CODES.VULNS_FOUND); @@ -118,7 +124,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); const jsonOutput = JSON.parse(stdout); @@ -147,7 +155,9 @@ describe('snyk test --reachability', () => { expect(code).not.toBe(EXIT_CODES.ERROR); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); const jsonOutput = JSON.parse(stdout); @@ -171,7 +181,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); const jsonOutput = JSON.parse(stdout); @@ -196,7 +208,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); const jsonOutput = JSON.parse(stdout); @@ -285,7 +299,9 @@ describe('snyk test --reachability', () => { ); expect(stdout).not.toBe(''); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); const jsonOutput = JSON.parse(stdout); expect(jsonOutput.vulnerabilities.length).toBeGreaterThanOrEqual(1); @@ -312,7 +328,9 @@ describe('snyk test --reachability', () => { }, ); - expect(stderr).toBe(''); + expect(stderr).toContain( + 'WARNING: --print-effective-graph is deprecated. Use --print-graph --prune instead.', + ); expect(stdout).toContain('Test Summary'); const jsonOutput = JSON.parse(readFileSync(jsonOutputPath, 'utf8')); From f59b51378250d8820fc90a54cd34b1a7278199e5 Mon Sep 17 00:00:00 2001 From: neema-beglou-snyk Date: Tue, 9 Jun 2026 16:07:57 +0100 Subject: [PATCH 3/4] fix: map legacy flags in allow-incomplete-sbom tests and apply prettier [CSENG-190] --- src/lib/ecosystems/test.ts | 5 +-- src/lib/plugins/get-deps-from-plugin.ts | 1 - src/lib/snyk-test/common.ts | 28 +++++++++---- src/lib/snyk-test/index.js | 6 ++- src/lib/snyk-test/run-test.ts | 5 +-- src/lib/types.ts | 4 +- test/jest/unit/allow-incomplete-sbom.spec.ts | 8 ++++ .../unit/print-graph-flag-resolution.spec.ts | 41 +++++++++++++++---- 8 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/lib/ecosystems/test.ts b/src/lib/ecosystems/test.ts index 2029e9cc4a..dfd26653bd 100644 --- a/src/lib/ecosystems/test.ts +++ b/src/lib/ecosystems/test.ts @@ -56,10 +56,7 @@ export async function testEcosystem( } spinner.clearAll(); - if ( - isUnmanagedEcosystem(ecosystem) && - shouldPrintGraph(options) - ) { + if (isUnmanagedEcosystem(ecosystem) && shouldPrintGraph(options)) { const [target] = paths; return printUnmanagedDepGraph(results, target, process.stdout, options); } diff --git a/src/lib/plugins/get-deps-from-plugin.ts b/src/lib/plugins/get-deps-from-plugin.ts index ab2afcd294..f818b9e30d 100644 --- a/src/lib/plugins/get-deps-from-plugin.ts +++ b/src/lib/plugins/get-deps-from-plugin.ts @@ -150,7 +150,6 @@ export async function getDepsFromPlugin( errMessage: error.userMessage, }, ], - } as MultiProjectResultCustom; } throw error; diff --git a/src/lib/snyk-test/common.ts b/src/lib/snyk-test/common.ts index 55153a3594..24eed59e47 100644 --- a/src/lib/snyk-test/common.ts +++ b/src/lib/snyk-test/common.ts @@ -168,11 +168,12 @@ export function shouldEmbedErrors(opts: Options): boolean { } export function shouldPrintGraph(opts: Options): boolean { - return !opts['print-deps'] && ( - !!opts['print-graph'] || - !!opts['print-effective-graph'] || - !!opts['print-effective-graph-with-errors'] || - !!opts['print-output-jsonl-with-errors'] + return ( + !opts['print-deps'] && + (!!opts['print-graph'] || + !!opts['print-effective-graph'] || + !!opts['print-effective-graph-with-errors'] || + !!opts['print-output-jsonl-with-errors']) ); } @@ -194,15 +195,25 @@ export function mapLegacyGraphFlags(opts: Options): void { return; } - const legacyMappings: Array<{ flag: keyof Options; prune: boolean; embedErrors: boolean }> = [ + const legacyMappings: Array<{ + flag: keyof Options; + prune: boolean; + embedErrors: boolean; + }> = [ { flag: 'print-effective-graph', prune: true, embedErrors: false }, - { flag: 'print-effective-graph-with-errors', prune: true, embedErrors: true }, + { + flag: 'print-effective-graph-with-errors', + prune: true, + embedErrors: true, + }, { flag: 'print-output-jsonl-with-errors', prune: false, embedErrors: true }, ]; for (const { flag, prune, embedErrors } of legacyMappings) { if (opts[flag]) { - const replacement = prune ? '--print-graph --prune' : '--print-graph --jsonl'; + const replacement = prune + ? '--print-graph --prune' + : '--print-graph --jsonl'; process.stderr.write( `WARNING: --${flag} is deprecated. Use ${replacement} instead.\n`, ); @@ -285,7 +296,6 @@ export async function printDepGraphError( }); } - /** * getOrCreateErrorCatalogError returns a ProblemError instance for consistent error catalog usage. * This helper is used to ensure errors are wrapped in a ProblemError so they can be reported in a standardized way, diff --git a/src/lib/snyk-test/index.js b/src/lib/snyk-test/index.js index bb4b36fcf2..5a041d66f9 100644 --- a/src/lib/snyk-test/index.js +++ b/src/lib/snyk-test/index.js @@ -19,7 +19,11 @@ const { DISABLE_GO_PACKAGE_URLS_IN_CLI_FEATURE_FLAG, } = require('../package-managers'); const { getOrganizationID } = require('../organization'); -const { printDepGraphError, mapLegacyGraphFlags, shouldEmbedErrors } = require('./common'); +const { + printDepGraphError, + mapLegacyGraphFlags, + shouldEmbedErrors, +} = require('./common'); const debug = require('debug')('snyk-test'); async function test(root, options, callback) { diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 0530282da2..49a7ce8d21 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -394,10 +394,7 @@ export async function runTest( // At this point managed ecosystems have dependency graphs printed. // Containers however require another roundtrip to get all the // dependency graph artifacts for printing. - if ( - !options.docker && - shouldPrintGraph(options) - ) { + if (!options.docker && shouldPrintGraph(options)) { return []; } diff --git a/src/lib/types.ts b/src/lib/types.ts index e5c1d922bc..722abe3655 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -72,8 +72,8 @@ export interface Options { 'print-effective-graph'?: boolean; 'print-effective-graph-with-errors'?: boolean; 'print-output-jsonl-with-errors'?: boolean; - 'prune'?: boolean; - 'jsonl'?: boolean; + prune?: boolean; + jsonl?: boolean; 'embed-errors'?: boolean; 'remote-repo-url'?: string; criticality?: string; diff --git a/test/jest/unit/allow-incomplete-sbom.spec.ts b/test/jest/unit/allow-incomplete-sbom.spec.ts index 9d82076242..17346e4fa8 100644 --- a/test/jest/unit/allow-incomplete-sbom.spec.ts +++ b/test/jest/unit/allow-incomplete-sbom.spec.ts @@ -4,6 +4,7 @@ import { getMultiPluginResult, MultiProjectResultCustom, } from '../../../src/lib/plugins/get-multi-plugin-result'; +import { mapLegacyGraphFlags } from '../../../src/lib/snyk-test/common'; import { Options, TestOptions } from '../../../src/lib/types'; import { createProject, @@ -29,6 +30,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { path: project.path(), 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = (await getDepsFromPlugin( project.path(), @@ -67,6 +69,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { packageManager: 'npm', 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = (await getDepsFromPlugin( project.path(), @@ -95,6 +98,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { allProjects: true, 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = await getMultiPluginResult( project.path(), @@ -128,6 +132,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { allProjects: true, 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = await getMultiPluginResult( project.path(), @@ -205,6 +210,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { allProjects: true, 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = (await getDepsFromPlugin( project.path(), @@ -227,6 +233,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { allProjects: true, 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = (await getDepsFromPlugin( project.path(), @@ -247,6 +254,7 @@ describe('allow-incomplete-sbom: error handling in plugin layer', () => { allProjects: true, 'print-output-jsonl-with-errors': true, }; + mapLegacyGraphFlags(options); const result = (await getDepsFromPlugin( project.path(), diff --git a/test/jest/unit/print-graph-flag-resolution.spec.ts b/test/jest/unit/print-graph-flag-resolution.spec.ts index 2ab2a40c44..ba35aa5262 100644 --- a/test/jest/unit/print-graph-flag-resolution.spec.ts +++ b/test/jest/unit/print-graph-flag-resolution.spec.ts @@ -15,7 +15,9 @@ describe('print-graph flag resolution', () => { }; beforeEach(() => { - stderrSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true); + stderrSpy = jest + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); }); afterEach(() => { @@ -34,7 +36,11 @@ describe('print-graph flag resolution', () => { }); it('--print-graph --jsonl resolves to complete JSONL (no pruning)', () => { - const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, jsonl: true }; + const opts: Options & TestOptions = { + ...baseOptions, + 'print-graph': true, + jsonl: true, + }; mapLegacyGraphFlags(opts); expect(shouldPrintGraph(opts)).toBe(true); @@ -45,7 +51,11 @@ describe('print-graph flag resolution', () => { }); it('--print-graph --prune resolves to pruned JSONL', () => { - const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, prune: true }; + const opts: Options & TestOptions = { + ...baseOptions, + 'print-graph': true, + prune: true, + }; mapLegacyGraphFlags(opts); expect(shouldPrintGraph(opts)).toBe(true); @@ -66,7 +76,10 @@ describe('print-graph flag resolution', () => { }); it('--print-effective-graph maps to pruned JSONL but throws on errors', () => { - const opts: Options & TestOptions = { ...baseOptions, 'print-effective-graph': true }; + const opts: Options & TestOptions = { + ...baseOptions, + 'print-effective-graph': true, + }; mapLegacyGraphFlags(opts); expect(shouldPrintGraph(opts)).toBe(true); @@ -80,7 +93,10 @@ describe('print-graph flag resolution', () => { }); it('--print-effective-graph-with-errors maps to pruned JSONL with embedded errors', () => { - const opts: Options & TestOptions = { ...baseOptions, 'print-effective-graph-with-errors': true }; + const opts: Options & TestOptions = { + ...baseOptions, + 'print-effective-graph-with-errors': true, + }; mapLegacyGraphFlags(opts); expect(shouldPrintGraph(opts)).toBe(true); @@ -89,12 +105,17 @@ describe('print-graph flag resolution', () => { expect(opts['print-graph']).toBe(true); expect(shouldEmbedErrors(opts)).toBe(true); expect(stderrSpy).toHaveBeenCalledWith( - expect.stringContaining('--print-effective-graph-with-errors is deprecated'), + expect.stringContaining( + '--print-effective-graph-with-errors is deprecated', + ), ); }); it('--print-output-jsonl-with-errors maps to unpruned JSONL with embedded errors', () => { - const opts: Options & TestOptions = { ...baseOptions, 'print-output-jsonl-with-errors': true }; + const opts: Options & TestOptions = { + ...baseOptions, + 'print-output-jsonl-with-errors': true, + }; mapLegacyGraphFlags(opts); expect(shouldPrintGraph(opts)).toBe(true); @@ -118,7 +139,11 @@ describe('print-graph flag resolution', () => { }); it('--print-deps alongside --print-graph suppresses graph printing', () => { - const opts: Options & TestOptions = { ...baseOptions, 'print-graph': true, 'print-deps': true }; + const opts: Options & TestOptions = { + ...baseOptions, + 'print-graph': true, + 'print-deps': true, + }; mapLegacyGraphFlags(opts); expect(shouldPrintGraph(opts)).toBe(false); From 6bd1db64c92e37731a83e540cfc493b02023b21e Mon Sep 17 00:00:00 2001 From: neema-beglou-snyk Date: Fri, 12 Jun 2026 17:06:57 +0100 Subject: [PATCH 4/4] fix: gofmt cliv2 files [CSENG-190] --- cliv2/cmd/cliv2/behavior/maperrortoexitcode.go | 8 ++++---- cliv2/internal/cliv2/cliv2.go | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go index 488ea7056d..c6b1fd0efe 100644 --- a/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go +++ b/cliv2/cmd/cliv2/behavior/maperrortoexitcode.go @@ -15,10 +15,10 @@ var MapErrorCatalogToExitCode func(err *snyk_errors.Error, defaultValue int) int // mapErrorToExitCode maps error catalog errors to exit codes. Please extend the switch statement if new error codes need to be mapped. func mapErrorToExitCode(err *snyk_errors.Error, defaultValue int) int { var errorCatalogToExitCodeMap = map[string]int{ - code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - cli.NewNoSupportedFilesFoundError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, - snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL, + code.NewUnsupportedProjectError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + aibom.NewNoSupportedFilesError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + cli.NewNoSupportedFilesFoundError("").ErrorCode: constants.SNYK_EXIT_CODE_UNSUPPORTED_PROJECTS, + snyk.NewMaintenanceWindowError("").ErrorCode: constants.SNYK_EXIT_CODE_EX_TEMPFAIL, // Add new mappings here } diff --git a/cliv2/internal/cliv2/cliv2.go b/cliv2/internal/cliv2/cliv2.go index 3569eda66e..e7d4433abc 100644 --- a/cliv2/internal/cliv2/cliv2.go +++ b/cliv2/internal/cliv2/cliv2.go @@ -605,7 +605,6 @@ func DeriveExitCode(err error) int { return returnCode } - func (e EnvironmentWarning) Error() string { return e.message }