t / chainlint.sedon commit Merge branch 'jt/commit-graph-per-object-store' (3bc484a)
   1#------------------------------------------------------------------------------
   2# Detect broken &&-chains in tests.
   3#
   4# At present, only &&-chains in subshells are examined by this linter;
   5# top-level &&-chains are instead checked directly by the test framework. Like
   6# the top-level &&-chain linter, the subshell linter (intentionally) does not
   7# check &&-chains within {...} blocks.
   8#
   9# Checking for &&-chain breakage is done line-by-line by pure textual
  10# inspection.
  11#
  12# Incomplete lines (those ending with "\") are stitched together with following
  13# lines to simplify processing, particularly of "one-liner" statements.
  14# Top-level here-docs are swallowed to avoid false positives within the
  15# here-doc body, although the statement to which the here-doc is attached is
  16# retained.
  17#
  18# Heuristics are used to detect end-of-subshell when the closing ")" is cuddled
  19# with the final subshell statement on the same line:
  20#
  21#    (cd foo &&
  22#        bar)
  23#
  24# in order to avoid misinterpreting the ")" in constructs such as "x=$(...)"
  25# and "case $x in *)" as ending the subshell.
  26#
  27# Lines missing a final "&&" are flagged with "?!AMP?!", and lines which chain
  28# commands with ";" internally rather than "&&" are flagged "?!SEMI?!". A line
  29# may be flagged for both violations.
  30#
  31# Detection of a missing &&-link in a multi-line subshell is complicated by the
  32# fact that the last statement before the closing ")" must not end with "&&".
  33# Since processing is line-by-line, it is not known whether a missing "&&" is
  34# legitimate or not until the _next_ line is seen. To accommodate this, within
  35# multi-line subshells, each line is stored in sed's "hold" area until after
  36# the next line is seen and processed. If the next line is a stand-alone ")",
  37# then a missing "&&" on the previous line is legitimate; otherwise a missing
  38# "&&" is a break in the &&-chain.
  39#
  40#    (
  41#         cd foo &&
  42#         bar
  43#    )
  44#
  45# In practical terms, when "bar" is encountered, it is flagged with "?!AMP?!",
  46# but when the stand-alone ")" line is seen which closes the subshell, the
  47# "?!AMP?!" violation is removed from the "bar" line (retrieved from the "hold"
  48# area) since the final statement of a subshell must not end with "&&". The
  49# final line of a subshell may still break the &&-chain by using ";" internally
  50# to chain commands together rather than "&&", so "?!SEMI?!" is never removed
  51# from a line (even though "?!AMP?!" might be).
  52#
  53# Care is taken to recognize the last _statement_ of a multi-line subshell, not
  54# necessarily the last textual _line_ within the subshell, since &&-chaining
  55# applies to statements, not to lines. Consequently, blank lines, comment
  56# lines, and here-docs are swallowed (but not the command to which the here-doc
  57# is attached), leaving the last statement in the "hold" area, not the last
  58# line, thus simplifying &&-link checking.
  59#
  60# The final statement before "done" in for- and while-loops, and before "elif",
  61# "else", and "fi" in if-then-else likewise must not end with "&&", thus
  62# receives similar treatment.
  63#
  64# To facilitate regression testing (and manual debugging), a ">" annotation is
  65# applied to the line containing ")" which closes a subshell, ">>" to a line
  66# closing a nested subshell, and ">>>" to a line closing both at once. This
  67# makes it easy to detect whether the heuristics correctly identify
  68# end-of-subshell.
  69#------------------------------------------------------------------------------
  70
  71# incomplete line -- slurp up next line
  72:squash
  73/\\$/ {
  74        N
  75        s/\\\n//
  76        bsquash
  77}
  78
  79# here-doc -- swallow it to avoid false hits within its body (but keep the
  80# command to which it was attached)
  81/<<[    ]*[-\\]*EOF[    ]*/ {
  82        s/[     ]*<<[   ]*[-\\]*EOF//
  83        h
  84        :hereslurp
  85        N
  86        s/.*\n//
  87        /^[     ]*EOF[  ]*$/!bhereslurp
  88        x
  89}
  90
  91# one-liner "(...) &&"
  92/^[     ]*!*[   ]*(..*)[        ]*&&[   ]*$/boneline
  93
  94# same as above but without trailing "&&"
  95/^[     ]*!*[   ]*(..*)[        ]*$/boneline
  96
  97# one-liner "(...) >x" (or "2>x" or "<x" or "|x" or "&"
  98/^[     ]*!*[   ]*(..*)[        ]*[0-9]*[<>|&]/boneline
  99
 100# multi-line "(...\n...)"
 101/^[     ]*(/bsubshell
 102
 103# innocuous line -- print it and advance to next line
 104b
 105
 106# found one-liner "(...)" -- mark suspect if it uses ";" internally rather than
 107# "&&" (but not ";" in a string)
 108:oneline
 109/;/{
 110        /"[^"]*;[^"]*"/!s/^/?!SEMI?!/
 111}
 112b
 113
 114:subshell
 115# bare "(" line?
 116/^[     ]*([    ]*$/ {
 117        # stash for later printing
 118        h
 119        bnextline
 120}
 121# "(..." line -- split off and stash "(", then process "..." as its own line
 122x
 123s/.*/(/
 124x
 125s/(//
 126bslurp
 127
 128:nextline
 129N
 130s/.*\n//
 131
 132:slurp
 133# incomplete line "...\"
 134/\\$/bincomplete
 135# multi-line quoted string "...\n..."
 136/^[^"]*"[^"]*$/bdqstring
 137# multi-line quoted string '...\n...' (but not contraction in string "it's so")
 138/^[^']*'[^']*$/{
 139        /"[^'"]*'[^'"]*"/!bsqstring
 140}
 141# here-doc -- swallow it
 142/<<[    ]*[-\\]*EOF/bheredoc
 143/<<[    ]*[-\\]*EOT/bheredoc
 144/<<[    ]*[-\\]*INPUT_END/bheredoc
 145# comment or empty line -- discard since final non-comment, non-empty line
 146# before closing ")", "done", "elsif", "else", or "fi" will need to be
 147# re-visited to drop "suspect" marking since final line of those constructs
 148# legitimately lacks "&&", so "suspect" mark must be removed
 149/^[     ]*#/bnextline
 150/^[     ]*$/bnextline
 151# in-line comment -- strip it (but not "#" in a string, Bash ${#...} array
 152# length, or Perforce "//depot/path#42" revision in filespec)
 153/[      ]#/{
 154        /"[^"]*#[^"]*"/!s/[     ]#.*$//
 155}
 156# one-liner "case ... esac"
 157/^[     ]*case[         ]*..*esac/bcheckchain
 158# multi-line "case ... esac"
 159/^[     ]*case[         ]..*[   ]in/bcase
 160# multi-line "for ... done" or "while ... done"
 161/^[     ]*for[  ]..*[   ]in/bcontinue
 162/^[     ]*while[        ]/bcontinue
 163/^[     ]*do[   ]/bcontinue
 164/^[     ]*do[   ]*$/bcontinue
 165/;[     ]*do/bcontinue
 166/^[     ]*done[         ]*&&[   ]*$/bdone
 167/^[     ]*done[         ]*$/bdone
 168/^[     ]*done[         ]*[<>|]/bdone
 169/^[     ]*done[         ]*)/bdone
 170/||[    ]*exit[         ]/bcontinue
 171/||[    ]*exit[         ]*$/bcontinue
 172# multi-line "if...elsif...else...fi"
 173/^[     ]*if[   ]/bcontinue
 174/^[     ]*then[         ]/bcontinue
 175/^[     ]*then[         ]*$/bcontinue
 176/;[     ]*then/bcontinue
 177/^[     ]*elif[         ]/belse
 178/^[     ]*elif[         ]*$/belse
 179/^[     ]*else[         ]/belse
 180/^[     ]*else[         ]*$/belse
 181/^[     ]*fi[   ]*&&[   ]*$/bdone
 182/^[     ]*fi[   ]*$/bdone
 183/^[     ]*fi[   ]*[<>|]/bdone
 184/^[     ]*fi[   ]*)/bdone
 185# nested one-liner "(...) &&"
 186/^[     ]*(.*)[         ]*&&[   ]*$/bcheckchain
 187# nested one-liner "(...)"
 188/^[     ]*(.*)[         ]*$/bcheckchain
 189# nested one-liner "(...) >x" (or "2>x" or "<x" or "|x")
 190/^[     ]*(.*)[         ]*[0-9]*[<>|]/bcheckchain
 191# nested multi-line "(...\n...)"
 192/^[     ]*(/bnest
 193# multi-line "{...\n...}"
 194/^[     ]*{/bblock
 195# closing ")" on own line -- exit subshell
 196/^[     ]*)/bclosesolo
 197# "$((...))" -- arithmetic expansion; not closing ")"
 198/\$(([^)][^)]*))[^)]*$/bcheckchain
 199# "$(...)" -- command substitution; not closing ")"
 200/\$([^)][^)]*)[^)]*$/bcheckchain
 201# multi-line "$(...\n...)" -- command substitution; treat as nested subshell
 202/\$([   ]*$/bnest
 203# "=(...)" -- Bash array assignment; not closing ")"
 204/=(/bcheckchain
 205# closing "...) &&"
 206/)[     ]*&&[   ]*$/bclose
 207# closing "...)"
 208/)[     ]*$/bclose
 209# closing "...) >x" (or "2>x" or "<x" or "|x")
 210/)[     ]*[<>|]/bclose
 211:checkchain
 212# mark suspect if line uses ";" internally rather than "&&" (but not ";" in a
 213# string and not ";;" in one-liner "case...esac")
 214/;/{
 215        /;;/!{
 216                /"[^"]*;[^"]*"/!s/^/?!SEMI?!/
 217        }
 218}
 219# line ends with pipe "...|" -- valid; not missing "&&"
 220/|[     ]*$/bcontinue
 221# missing end-of-line "&&" -- mark suspect
 222/&&[    ]*$/!s/^/?!AMP?!/
 223:continue
 224# retrieve and print previous line
 225x
 226n
 227bslurp
 228
 229# found incomplete line "...\" -- slurp up next line
 230:incomplete
 231N
 232s/\\\n//
 233bslurp
 234
 235# found multi-line double-quoted string "...\n..." -- slurp until end of string
 236:dqstring
 237s/"//g
 238N
 239s/\n//
 240/"/!bdqstring
 241bcheckchain
 242
 243# found multi-line single-quoted string '...\n...' -- slurp until end of string
 244:sqstring
 245s/'//g
 246N
 247s/\n//
 248/'/!bsqstring
 249bcheckchain
 250
 251# found here-doc -- swallow it to avoid false hits within its body (but keep
 252# the command to which it was attached); take care to handle here-docs nested
 253# within here-docs by only recognizing closing tag matching outer here-doc
 254# opening tag
 255:heredoc
 256/EOF/{ s/[      ]*<<[   ]*[-\\]*EOF//; s/^/EOF/; }
 257/EOT/{ s/[      ]*<<[   ]*[-\\]*EOT//; s/^/EOT/; }
 258/INPUT_END/{ s/[        ]*<<[   ]*[-\\]*INPUT_END//; s/^/INPUT_END/; }
 259:hereslurpsub
 260N
 261/^EOF.*\n[      ]*EOF[  ]*$/bhereclose
 262/^EOT.*\n[      ]*EOT[  ]*$/bhereclose
 263/^INPUT_END.*\n[        ]*INPUT_END[    ]*$/bhereclose
 264bhereslurpsub
 265:hereclose
 266s/^EOF//
 267s/^EOT//
 268s/^INPUT_END//
 269s/\n.*$//
 270bcheckchain
 271
 272# found "case ... in" -- pass through untouched
 273:case
 274x
 275n
 276/^[     ]*esac/bslurp
 277bcase
 278
 279# found "else" or "elif" -- drop "suspect" from final line before "else" since
 280# that line legitimately lacks "&&"
 281:else
 282x
 283s/?!AMP?!//
 284x
 285bcontinue
 286
 287# found "done" closing for-loop or while-loop, or "fi" closing if-then -- drop
 288# "suspect" from final contained line since that line legitimately lacks "&&"
 289:done
 290x
 291s/?!AMP?!//
 292x
 293# is 'done' or 'fi' cuddled with ")" to close subshell?
 294/done.*)/bclose
 295/fi.*)/bclose
 296bcheckchain
 297
 298# found nested multi-line "(...\n...)" -- pass through untouched
 299:nest
 300x
 301:nestslurp
 302n
 303# closing ")" on own line -- stop nested slurp
 304/^[     ]*)/bnestclose
 305# comment -- not closing ")" if in comment
 306/^[     ]*#/bnestcontinue
 307# "$((...))" -- arithmetic expansion; not closing ")"
 308/\$(([^)][^)]*))[^)]*$/bnestcontinue
 309# "$(...)" -- command substitution; not closing ")"
 310/\$([^)][^)]*)[^)]*$/bnestcontinue
 311# closing "...)" -- stop nested slurp
 312/)/bnestclose
 313:nestcontinue
 314x
 315bnestslurp
 316:nestclose
 317s/^/>>/
 318# is it "))" which closes nested and parent subshells?
 319/)[     ]*)/bslurp
 320bcheckchain
 321
 322# found multi-line "{...\n...}" block -- pass through untouched
 323:block
 324x
 325n
 326# closing "}" -- stop block slurp
 327/}/bcheckchain
 328bblock
 329
 330# found closing ")" on own line -- drop "suspect" from final line of subshell
 331# since that line legitimately lacks "&&" and exit subshell loop
 332:closesolo
 333x
 334s/?!AMP?!//
 335p
 336x
 337s/^/>/
 338b
 339
 340# found closing "...)" -- exit subshell loop
 341:close
 342x
 343p
 344x
 345s/^/>/
 346b