Skip to content

Commit b286418

Browse files
committedMar 29, 2021
feat: v1 support for previously fixed reqs.txt
1 parent 0384020 commit b286418

File tree

7 files changed

+283
-211
lines changed

7 files changed

+283
-211
lines changed
 

‎packages/snyk-fix/src/plugins/python/handlers/pip-requirements/index.ts

+90-32
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as debugLib from 'debug';
22
import * as pathLib from 'path';
3+
const sortBy = require('lodash.sortby');
4+
const groupBy = require('lodash.groupby');
35

46
import {
5-
DependencyPins,
67
EntityToFix,
78
FixChangesSummary,
89
FixOptions,
@@ -15,14 +16,10 @@ import { MissingRemediationDataError } from '../../../../lib/errors/missing-reme
1516
import { MissingFileNameError } from '../../../../lib/errors/missing-file-name';
1617
import { partitionByFixable } from './is-supported';
1718
import { NoFixesCouldBeAppliedError } from '../../../../lib/errors/no-fixes-applied';
18-
import {
19-
extractProvenance,
20-
PythonProvenance,
21-
} from './extract-version-provenance';
19+
import { extractProvenance } from './extract-version-provenance';
2220
import {
2321
ParsedRequirements,
2422
parseRequirementsFile,
25-
Requirement,
2623
} from './update-dependencies/requirements-file-parser';
2724

2825
const debug = debugLib('snyk-fix:python:requirements.txt');
@@ -38,27 +35,23 @@ export async function pipRequirementsTxt(
3835
skipped: [],
3936
};
4037

41-
const { fixable, skipped } = await partitionByFixable(entities);
42-
handlerResult.skipped.push(...skipped);
38+
const { fixable, skipped: notFixable } = await partitionByFixable(entities);
39+
handlerResult.skipped.push(...notFixable);
4340

44-
for (const entity of fixable) {
45-
try {
46-
const { changes } = await applyAllFixes(
47-
entity,
48-
// dir,
49-
// base,
50-
// remediation,
51-
// provenance,
52-
options,
53-
);
54-
if (!changes.length) {
55-
debug('Manifest has not changed!');
56-
throw new NoFixesCouldBeAppliedError();
57-
}
58-
handlerResult.succeeded.push({ original: entity, changes });
59-
} catch (e) {
60-
handlerResult.failed.push({ original: entity, error: e });
61-
}
41+
const ordered = sortByDirectory(fixable);
42+
const fixedFilesCache: string[] = [];
43+
for (const dir of Object.keys(ordered)) {
44+
debug(`Fixing entities in directory ${dir}`);
45+
const entitiesPerDirectory = ordered[dir].map((e) => e.entity);
46+
const { failed, succeeded, skipped, fixedFiles } = await fixAll(
47+
entitiesPerDirectory,
48+
options,
49+
fixedFilesCache,
50+
);
51+
fixedFilesCache.push(...fixedFiles);
52+
handlerResult.succeeded.push(...succeeded);
53+
handlerResult.failed.push(...failed);
54+
handlerResult.skipped.push(...skipped);
6255
}
6356
return handlerResult;
6457
}
@@ -85,6 +78,42 @@ export function getRequiredData(
8578
return { targetFile, remediation, workspace };
8679
}
8780

81+
async function fixAll(
82+
entities: EntityToFix[],
83+
options: FixOptions,
84+
fixedCache: string[],
85+
): Promise<PluginFixResponse & { fixedFiles: string[] }> {
86+
const handlerResult: PluginFixResponse = {
87+
succeeded: [],
88+
failed: [],
89+
skipped: [],
90+
};
91+
for (const entity of entities) {
92+
const targetFile = entity.scanResult.identity.targetFile!;
93+
try {
94+
const { dir, base } = pathLib.parse(targetFile);
95+
// parse & join again to support correct separator
96+
if (fixedCache.includes(pathLib.join(dir, base))) {
97+
handlerResult.succeeded.push({
98+
original: entity,
99+
changes: [{ success: true, userMessage: 'Previously fixed' }],
100+
});
101+
continue;
102+
}
103+
const { changes, fixedFiles } = await applyAllFixes(entity, options);
104+
if (!changes.length) {
105+
debug('Manifest has not changed!');
106+
throw new NoFixesCouldBeAppliedError();
107+
}
108+
fixedCache.push(...fixedFiles);
109+
handlerResult.succeeded.push({ original: entity, changes });
110+
} catch (e) {
111+
debug(`Failed to fix ${targetFile}.\nERROR: ${e}`);
112+
handlerResult.failed.push({ original: entity, error: e });
113+
}
114+
}
115+
return { ...handlerResult, fixedFiles: [] };
116+
}
88117
// TODO: optionally verify the deps install
89118
export async function fixIndividualRequirementsTxt(
90119
workspace: Workspace,
@@ -103,7 +132,12 @@ export async function fixIndividualRequirementsTxt(
103132
directUpgradesOnly,
104133
pathLib.join(dir, entryFileName) !== fullFilePath ? fileName : undefined,
105134
);
106-
if (!options.dryRun && changes.length > 0) {
135+
136+
if (!changes.length) {
137+
return { changes, appliedRemediation };
138+
}
139+
140+
if (!options.dryRun) {
107141
debug('Writing changes to file');
108142
await workspace.writeFile(pathLib.join(dir, fileName), updatedManifest);
109143
} else {
@@ -116,14 +150,16 @@ export async function fixIndividualRequirementsTxt(
116150
export async function applyAllFixes(
117151
entity: EntityToFix,
118152
options: FixOptions,
119-
): Promise<{ changes: FixChangesSummary[] }> {
153+
): Promise<{ changes: FixChangesSummary[]; fixedFiles: string[] }> {
120154
const { remediation, targetFile: entryFileName, workspace } = getRequiredData(
121155
entity,
122156
);
157+
const fixedFiles: string[] = [];
123158
const { dir, base } = pathLib.parse(entryFileName);
124159
const provenance = await extractProvenance(workspace, dir, base);
125160
const upgradeChanges: FixChangesSummary[] = [];
126161
const appliedUpgradeRemediation: string[] = [];
162+
/* Apply all upgrades first across all files that are included */
127163
for (const fileName of Object.keys(provenance)) {
128164
const skipApplyingPins = true;
129165
const { changes, appliedRemediation } = await fixIndividualRequirementsTxt(
@@ -137,10 +173,11 @@ export async function applyAllFixes(
137173
skipApplyingPins,
138174
);
139175
appliedUpgradeRemediation.push(...appliedRemediation);
140-
// what if we saw the file before and already fixed it?
141176
upgradeChanges.push(...changes);
177+
fixedFiles.push(pathLib.join(dir, fileName));
142178
}
143-
// now do left overs as pins + add tests
179+
180+
/* Apply all left over remediation as pins in the entry targetFile */
144181
const requirementsTxt = await workspace.readFile(entryFileName);
145182

146183
const toPin: RemediationChanges = filterOutAppliedUpgrades(
@@ -159,7 +196,7 @@ export async function applyAllFixes(
159196
directUpgradesOnly,
160197
);
161198

162-
return { changes: [...upgradeChanges, ...pinnedChanges] };
199+
return { changes: [...upgradeChanges, ...pinnedChanges], fixedFiles };
163200
}
164201

165202
function filterOutAppliedUpgrades(
@@ -168,7 +205,7 @@ function filterOutAppliedUpgrades(
168205
): RemediationChanges {
169206
const pinRemediation: RemediationChanges = {
170207
...remediation,
171-
pin: {}, // delete the pin remediation so we can add only not applied
208+
pin: {}, // delete the pin remediation so we can collect un-applied remediation
172209
};
173210
const pins = remediation.pin;
174211
const lowerCasedAppliedRemediation = appliedRemediation.map((i) =>
@@ -181,3 +218,24 @@ function filterOutAppliedUpgrades(
181218
}
182219
return pinRemediation;
183220
}
221+
222+
function sortByDirectory(
223+
entities: EntityToFix[],
224+
): {
225+
[dir: string]: Array<{
226+
entity: EntityToFix;
227+
dir: string;
228+
base: string;
229+
ext: string;
230+
root: string;
231+
name: string;
232+
}>;
233+
} {
234+
const mapped = entities.map((e) => ({
235+
entity: e,
236+
...pathLib.parse(e.scanResult.identity.targetFile!),
237+
}));
238+
239+
const sorted = sortBy(mapped, 'dir');
240+
return groupBy(sorted, 'dir');
241+
}

‎packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/__snapshots__/update-dependencies.spec.ts.snap

+37
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,43 @@ Array [
1313
]
1414
`;
1515

16+
exports[`fix *req*.txt / *.txt Python projects fixes multiple files via -r with the same name (some were already fixed) 1`] = `
17+
"Successful fixes:
18+
19+
app-with-already-fixed/requirements.txt
20+
✔ Upgraded Django from 1.6.1 to 2.0.1
21+
✔ Upgraded Django from 1.6.1 to 2.0.1 (upgraded in core/requirements.txt)
22+
✔ Upgraded Jinja2 from 2.7.2 to 2.7.3 (upgraded in lib/requirements.txt)
23+
24+
app-with-already-fixed/core/requirements.txt
25+
✔ Previously fixed
26+
27+
app-with-already-fixed/lib/requirements.txt
28+
✔ Previously fixed
29+
30+
Summary:
31+
32+
0 items were not fixed
33+
3 items were successfully fixed"
34+
`;
35+
36+
exports[`fix *req*.txt / *.txt Python projects fixes multiple files via -r with the same name (some were already fixed) 2`] = `
37+
Array [
38+
Object {
39+
"success": true,
40+
"userMessage": "Upgraded Django from 1.6.1 to 2.0.1",
41+
},
42+
Object {
43+
"success": true,
44+
"userMessage": "Upgraded Django from 1.6.1 to 2.0.1 (upgraded in core/requirements.txt)",
45+
},
46+
Object {
47+
"success": true,
48+
"userMessage": "Upgraded Jinja2 from 2.7.2 to 2.7.3 (upgraded in lib/requirements.txt)",
49+
},
50+
]
51+
`;
52+
1653
exports[`fix *req*.txt / *.txt Python projects retains python markers 1`] = `
1754
"amqp==2.4.2
1855
apscheduler==3.6.0

‎packages/snyk-fix/test/acceptance/plugins/python/update-dependencies/update-dependencies.spec.ts

+149-179
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
click>7.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Django==1.6.1 ; python_version > '1.0'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Jinja2==2.7.2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-r core/requirements.txt
2+
-r lib/requirements.txt
3+
-r base.txt
4+
Django==1.6.1

0 commit comments

Comments
 (0)
Please sign in to comment.