1
1
import * as debugLib from 'debug' ;
2
2
import * as pathLib from 'path' ;
3
+ const sortBy = require ( 'lodash.sortby' ) ;
4
+ const groupBy = require ( 'lodash.groupby' ) ;
3
5
4
6
import {
5
- DependencyPins ,
6
7
EntityToFix ,
7
8
FixChangesSummary ,
8
9
FixOptions ,
@@ -15,14 +16,10 @@ import { MissingRemediationDataError } from '../../../../lib/errors/missing-reme
15
16
import { MissingFileNameError } from '../../../../lib/errors/missing-file-name' ;
16
17
import { partitionByFixable } from './is-supported' ;
17
18
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' ;
22
20
import {
23
21
ParsedRequirements ,
24
22
parseRequirementsFile ,
25
- Requirement ,
26
23
} from './update-dependencies/requirements-file-parser' ;
27
24
28
25
const debug = debugLib ( 'snyk-fix:python:requirements.txt' ) ;
@@ -38,27 +35,23 @@ export async function pipRequirementsTxt(
38
35
skipped : [ ] ,
39
36
} ;
40
37
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 ) ;
43
40
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 ) ;
62
55
}
63
56
return handlerResult ;
64
57
}
@@ -85,6 +78,42 @@ export function getRequiredData(
85
78
return { targetFile, remediation, workspace } ;
86
79
}
87
80
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
+ }
88
117
// TODO: optionally verify the deps install
89
118
export async function fixIndividualRequirementsTxt (
90
119
workspace : Workspace ,
@@ -103,7 +132,12 @@ export async function fixIndividualRequirementsTxt(
103
132
directUpgradesOnly ,
104
133
pathLib . join ( dir , entryFileName ) !== fullFilePath ? fileName : undefined ,
105
134
) ;
106
- if ( ! options . dryRun && changes . length > 0 ) {
135
+
136
+ if ( ! changes . length ) {
137
+ return { changes, appliedRemediation } ;
138
+ }
139
+
140
+ if ( ! options . dryRun ) {
107
141
debug ( 'Writing changes to file' ) ;
108
142
await workspace . writeFile ( pathLib . join ( dir , fileName ) , updatedManifest ) ;
109
143
} else {
@@ -116,14 +150,16 @@ export async function fixIndividualRequirementsTxt(
116
150
export async function applyAllFixes (
117
151
entity : EntityToFix ,
118
152
options : FixOptions ,
119
- ) : Promise < { changes : FixChangesSummary [ ] } > {
153
+ ) : Promise < { changes : FixChangesSummary [ ] ; fixedFiles : string [ ] } > {
120
154
const { remediation, targetFile : entryFileName , workspace } = getRequiredData (
121
155
entity ,
122
156
) ;
157
+ const fixedFiles : string [ ] = [ ] ;
123
158
const { dir, base } = pathLib . parse ( entryFileName ) ;
124
159
const provenance = await extractProvenance ( workspace , dir , base ) ;
125
160
const upgradeChanges : FixChangesSummary [ ] = [ ] ;
126
161
const appliedUpgradeRemediation : string [ ] = [ ] ;
162
+ /* Apply all upgrades first across all files that are included */
127
163
for ( const fileName of Object . keys ( provenance ) ) {
128
164
const skipApplyingPins = true ;
129
165
const { changes, appliedRemediation } = await fixIndividualRequirementsTxt (
@@ -137,10 +173,11 @@ export async function applyAllFixes(
137
173
skipApplyingPins ,
138
174
) ;
139
175
appliedUpgradeRemediation . push ( ...appliedRemediation ) ;
140
- // what if we saw the file before and already fixed it?
141
176
upgradeChanges . push ( ...changes ) ;
177
+ fixedFiles . push ( pathLib . join ( dir , fileName ) ) ;
142
178
}
143
- // now do left overs as pins + add tests
179
+
180
+ /* Apply all left over remediation as pins in the entry targetFile */
144
181
const requirementsTxt = await workspace . readFile ( entryFileName ) ;
145
182
146
183
const toPin : RemediationChanges = filterOutAppliedUpgrades (
@@ -159,7 +196,7 @@ export async function applyAllFixes(
159
196
directUpgradesOnly ,
160
197
) ;
161
198
162
- return { changes : [ ...upgradeChanges , ...pinnedChanges ] } ;
199
+ return { changes : [ ...upgradeChanges , ...pinnedChanges ] , fixedFiles } ;
163
200
}
164
201
165
202
function filterOutAppliedUpgrades (
@@ -168,7 +205,7 @@ function filterOutAppliedUpgrades(
168
205
) : RemediationChanges {
169
206
const pinRemediation : RemediationChanges = {
170
207
...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
172
209
} ;
173
210
const pins = remediation . pin ;
174
211
const lowerCasedAppliedRemediation = appliedRemediation . map ( ( i ) =>
@@ -181,3 +218,24 @@ function filterOutAppliedUpgrades(
181
218
}
182
219
return pinRemediation ;
183
220
}
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
+ }
0 commit comments