@@ -55,15 +55,30 @@ func (p *Preprocessor) CFG(graph *cfg.CFG, funcDecl *ast.FuncDecl) *cfg.CFG {
5555 failureBlock := & cfg.Block {Index : int32 (len (graph .Blocks ))}
5656 graph .Blocks = append (graph .Blocks , failureBlock )
5757
58- // Perform the (series of) CFG transformations.
58+ // Perform a series of CFG transformations here (for hooks and canonicalization). The order of
59+ // these transformations matters due to canonicalization. Some transformations may expect the
60+ // CFG to be in canonical form, and some transformations may change the CFG structure in a way
61+ // that it needs to be re-canonicalized.
62+
63+ // split blocks do not require the CFG to be in canonical form, and it may modify the CFG
64+ // structure in a way that it needs to be re-canonicalized. Here, we cleverly bundles the two
65+ // operations together such that we only need to run canonicalization once.
5966 for _ , block := range graph .Blocks {
6067 if block .Live {
6168 p .splitBlockOnTrustedFuncs (graph , block , failureBlock )
6269 }
6370 }
6471 for _ , block := range graph .Blocks {
6572 if block .Live {
66- p .restructureConditional (graph , block )
73+ p .canonicalizeConditional (graph , block )
74+ }
75+ }
76+ // Replacing conditionals in the CFG requires the CFG to be in canonical form (such that it
77+ // does not have to handle "trustedFunc() && trustedFunc()"), and it will canonicalize the
78+ // modified block by itself.
79+ for _ , block := range graph .Blocks {
80+ if block .Live {
81+ p .replaceConditional (graph , block )
6782 }
6883 }
6984
@@ -119,6 +134,10 @@ func copyGraph(graph *cfg.CFG) *cfg.CFG {
119134 return newGraph
120135}
121136
137+ // splitBlockOnTrustedFuncs splits the CFG block into two parts upon seeing a trusted function
138+ // from the hook framework (e.g., "require.Nil(t, arg)" to "if arg == nil { <all code after> }".
139+ // This does not expect the CFG to be in canonical form, and it may change the CFG structure in a
140+ // way that it needs to be re-canonicalized.
122141func (p * Preprocessor ) splitBlockOnTrustedFuncs (graph * cfg.CFG , thisBlock , failureBlock * cfg.Block ) {
123142 for i , node := range thisBlock .Nodes {
124143 expr , ok := node .(* ast.ExprStmt )
@@ -153,47 +172,69 @@ func (p *Preprocessor) splitBlockOnTrustedFuncs(graph *cfg.CFG, thisBlock, failu
153172 }
154173}
155174
156- func (p * Preprocessor ) restructureConditional (graph * cfg.CFG , thisBlock * cfg.Block ) {
157- // We only restructure non-empty branching blocks.
158- if len (thisBlock .Nodes ) == 0 || len (thisBlock .Succs ) != 2 {
175+ // replaceConditional calls the hook functions and replaces the conditional expressions in the CFG
176+ // with the returned equivalent expression for analysis.
177+ //
178+ // This function expects the CFG to be in canonical form to fully function (otherwise it may miss
179+ // cases like "trustedFunc() && trustedFunc()").
180+ //
181+ // It also calls canonicalizeConditional to canonicalize the transformed block such that the CFG
182+ // is still canonical.
183+ func (p * Preprocessor ) replaceConditional (graph * cfg.CFG , block * cfg.Block ) {
184+ // We only replace conditionals on branching blocks.
185+ if len (block .Nodes ) == 0 || len (block .Succs ) != 2 {
159186 return
160187 }
161- cond , ok := thisBlock .Nodes [len (thisBlock .Nodes )- 1 ].(ast.Expr )
188+ call , ok := block .Nodes [len (block .Nodes )- 1 ].(* ast.CallExpr )
162189 if ! ok {
163190 return
164191 }
192+ replaced := hook .ReplaceConditional (p .pass , call )
193+ if replaced == nil {
194+ return
195+ }
165196
166- // places a new given node into the last position of this block
167- replaceCond := func (node ast.Node ) {
168- thisBlock .Nodes [len (thisBlock .Nodes )- 1 ] = node
197+ block .Nodes [len (block .Nodes )- 1 ] = replaced
198+ // The returned expression may be a binary expression, so we need to canonicalize the CFG again
199+ // after such replacement.
200+ p .canonicalizeConditional (graph , block )
201+ }
202+
203+ // canonicalizeConditional canonicalizes the conditional CFG structures to make it easier to reason
204+ // about control flows later. For example, it rewrites
205+ // `if !cond {T} {F}` to `if cond {F} {T}` (swap successors), and rewrites
206+ // `if cond1 && cond2 {T} {F}` to `if cond1 {if cond2 {T} else {F}}{F}` (nesting).
207+ func (p * Preprocessor ) canonicalizeConditional (graph * cfg.CFG , thisBlock * cfg.Block ) {
208+ // We only restructure non-empty branching blocks.
209+ if len (thisBlock .Nodes ) == 0 || len (thisBlock .Succs ) != 2 {
210+ return
169211 }
170212
171213 trueBranch := thisBlock .Succs [0 ] // type *cfg.Block
172214 falseBranch := thisBlock .Succs [1 ] // type *cfg.Block
173215
174- replaceTrueBranch := func (block * cfg.Block ) {
175- thisBlock .Succs [0 ] = block
176- }
177- replaceFalseBranch := func (block * cfg.Block ) {
178- thisBlock .Succs [1 ] = block
179- }
216+ // A few helper functions to make the code more readable.
217+ replaceCond := func (node ast.Node ) { thisBlock .Nodes [len (thisBlock .Nodes )- 1 ] = node } // The conditional expr is the last node in the block.
218+ replaceTrueBranch := func (block * cfg.Block ) { thisBlock .Succs [0 ] = block }
219+ replaceFalseBranch := func (block * cfg.Block ) { thisBlock .Succs [1 ] = block }
220+ swapTrueFalseBranches := func () { replaceTrueBranch (falseBranch ); replaceFalseBranch (trueBranch ) }
180221
181- swapTrueFalseBranches := func () {
182- replaceTrueBranch ( falseBranch )
183- replaceFalseBranch ( trueBranch )
222+ cond , ok := thisBlock . Nodes [ len ( thisBlock . Nodes ) - 1 ].(ast. Expr )
223+ if ! ok {
224+ return
184225 }
185226
186227 switch cond := cond .(type ) {
187228 case * ast.ParenExpr :
188229 // if a parenexpr, strip and restart - this is done with recursion to account for ((((x)))) case
189230 replaceCond (cond .X )
190- p .restructureConditional (graph , thisBlock ) // recur within parens
231+ p .canonicalizeConditional (graph , thisBlock ) // recur within parens
191232 case * ast.UnaryExpr :
192233 if cond .Op == token .NOT {
193234 // swap successors - i.e. swap true and false branches
194235 swapTrueFalseBranches ()
195236 replaceCond (cond .X )
196- p .restructureConditional (graph , thisBlock ) // recur within NOT
237+ p .canonicalizeConditional (graph , thisBlock ) // recur within NOT
197238 }
198239 case * ast.BinaryExpr :
199240 // Logical AND and Logical OR actually require the exact same short circuiting behavior
@@ -214,8 +255,8 @@ func (p *Preprocessor) restructureConditional(graph *cfg.CFG, thisBlock *cfg.Blo
214255 replaceFalseBranch (newBlock )
215256 }
216257 graph .Blocks = append (graph .Blocks , newBlock )
217- p .restructureConditional (graph , thisBlock )
218- p .restructureConditional (graph , newBlock )
258+ p .canonicalizeConditional (graph , thisBlock )
259+ p .canonicalizeConditional (graph , newBlock )
219260 }
220261
221262 // Standardize binary expressions to be of the form `expr OP literal` by swapping `x` and `y`, if `x` is a literal.
@@ -277,8 +318,8 @@ func (p *Preprocessor) restructureConditional(graph *cfg.CFG, thisBlock *cfg.Blo
277318 Op : token .NOT ,
278319 X : x ,
279320 }
280- replaceCond (newCond ) // replaces `ok != true` with `!ok`
281- p .restructureConditional (graph , thisBlock ) // recur to swap true and false branches for the unary expr `!ok`
321+ replaceCond (newCond ) // replaces `ok != true` with `!ok`
322+ p .canonicalizeConditional (graph , thisBlock ) // recur to swap true and false branches for the unary expr `!ok`
282323 }
283324
284325 case token .EQL :
@@ -292,8 +333,8 @@ func (p *Preprocessor) restructureConditional(graph *cfg.CFG, thisBlock *cfg.Blo
292333 Op : token .NOT ,
293334 X : x ,
294335 }
295- replaceCond (newCond ) // replaces `ok == false` with `!ok`
296- p .restructureConditional (graph , thisBlock ) // recur to swap true and false branches for the unary expr `!ok`
336+ replaceCond (newCond ) // replaces `ok == false` with `!ok`
337+ p .canonicalizeConditional (graph , thisBlock ) // recur to swap true and false branches for the unary expr `!ok`
297338 }
298339 }
299340 }
0 commit comments