Skip to content

Commit 5d8096d

Browse files
committed
fix: preserve ]] in SC2292 suggestions (#3447)
1 parent 766a836 commit 5d8096d

4 files changed

Lines changed: 95 additions & 7 deletions

File tree

src/ShellCheck/Analytics.hs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import ShellCheck.AnalyzerLib hiding (producesComments)
2929
import ShellCheck.CFG
3030
import qualified ShellCheck.CFGAnalysis as CF
3131
import ShellCheck.Data
32+
import ShellCheck.Fixer (applyFix)
3233
import ShellCheck.Parser
3334
import ShellCheck.Prelude
3435
import ShellCheck.Interface
@@ -40,6 +41,7 @@ import Control.Monad.Identity
4041
import Control.Monad.State
4142
import Control.Monad.Writer hiding ((<>))
4243
import Control.Monad.Reader
44+
import Data.Array (listArray)
4345
import Data.Char
4446
import Data.Functor
4547
import Data.Function (on)
@@ -310,6 +312,17 @@ verifyTree f s = producesComments f s == Just True
310312
verifyNotTree :: (Parameters -> Token -> [TokenComment]) -> String -> Bool
311313
verifyNotTree f s = producesComments f s == Just False
312314

315+
verifyTreeFix :: (Parameters -> Token -> [TokenComment]) -> String -> String -> Bool
316+
verifyTreeFix f input expected =
317+
case runAndGetComments f input of
318+
Just comments ->
319+
let fixes = mapMaybe tcFix comments
320+
inputLines = lines input
321+
in
322+
not (null fixes)
323+
&& applyFix (mconcat fixes) (listArray (1, length inputLines) inputLines) == lines expected
324+
Nothing -> False
325+
313326
checkCommand str f t@(T_SimpleCommand id _ (cmd:rest))
314327
| t `isCommand` str = f cmd rest
315328
checkCommand _ _ _ = return ()
@@ -332,7 +345,7 @@ producesComments f s = not . null <$> runAndGetComments f s
332345
runAndGetComments f s = do
333346
let pr = pScript s
334347
root <- prRoot pr
335-
let spec = defaultSpec pr
348+
let spec = (defaultSpec pr) { asSourceText = s }
336349
let params = makeParameters spec
337350
return $
338351
filterByAnnotation spec params $
@@ -4689,6 +4702,11 @@ prop_checkRequireDoubleBracket1 = verifyTree checkRequireDoubleBracket "[ -x foo
46894702
prop_checkRequireDoubleBracket2 = verifyTree checkRequireDoubleBracket "[ foo -o bar ]"
46904703
prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]"
46914704
prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]"
4705+
prop_checkRequireDoubleBracket5 = verifyTreeFix checkRequireDoubleBracket "#!/bin/bash\n[ -n \"$witness_me\" ]" "#!/bin/bash\n[[ -n \"$witness_me\" ]]"
4706+
prop_checkRequireDoubleBracket6 = verifyTreeFix checkRequireDoubleBracket "#!/bin/bash\n[ -n \"$witness_me\" ]]" "#!/bin/bash\n[[ -n \"$witness_me\" ]]"
4707+
prop_checkRequireDoubleBracket7 = verifyTreeFix checkRequireDoubleBracket "#!/bin/bash\n[ -n \"$witness_me\" ]" "#!/bin/bash\n[[ -n \"$witness_me\" ]]"
4708+
prop_checkRequireDoubleBracket8 = verifyTreeFix checkRequireDoubleBracket "#!/bin/bash\n[\n -n \"$witness_me\"\n]]" "#!/bin/bash\n[[\n -n \"$witness_me\"\n]]"
4709+
prop_checkRequireDoubleBracket9 = verifyTreeFix checkRequireDoubleBracket "#!/bin/bash\n[\t-n \"$witness_me\"\t]]" "#!/bin/bash\n[[\t-n \"$witness_me\"\t]]"
46924710
checkRequireDoubleBracket params =
46934711
if (shellType params) `elem` [Bash, Ksh, BusyboxSh]
46944712
then nodeChecksToTreeCheck [check] params
@@ -4704,10 +4722,73 @@ checkRequireDoubleBracket params =
47044722
then
47054723
[
47064724
replaceStart (getId t) params 0 "[",
4707-
replaceEnd (getId t) params 0 "]"
4725+
replaceConditionEnd t
47084726
]
47094727
else []
47104728

4729+
replaceConditionEnd t@(T_Condition id _ s) =
4730+
let (_, conditionEnd) = tokenPositions params Map.! id
4731+
(_, innerEnd) = simpleConditionRange s
4732+
suffix = sourceSpan innerEnd conditionEnd
4733+
leadingWhitespace = takeWhile isSpace suffix
4734+
in
4735+
if dropWhile isSpace suffix == "]]"
4736+
then replaceSpan id innerEnd conditionEnd (leadingWhitespace ++ "]]")
4737+
else replaceEnd id params 0 "]"
4738+
4739+
simpleConditionRange s =
4740+
case s of
4741+
TC_Binary id _ _ lhs rhs -> foldl1 mergeRanges [tokenRange id, simpleConditionRange lhs, simpleConditionRange rhs]
4742+
TC_Nullary id _ t -> mergeRanges (tokenRange id) (tokenRange (getId t))
4743+
TC_Unary id _ _ t -> mergeRanges (tokenRange id) (tokenRange (getId t))
4744+
_ -> tokenRange (getId s)
4745+
4746+
tokenRange id = tokenPositions params Map.! id
4747+
mergeRanges (start1, end1) (start2, end2) = (min start1 start2, max end1 end2)
4748+
4749+
sourceSpan start end
4750+
| otherwise =
4751+
let sourceLines = lines (sourceText params)
4752+
startLine = fromIntegral (posLine start)
4753+
endLine = fromIntegral (posLine end)
4754+
lineAt n
4755+
| n >= 1 && n <= length sourceLines = Just (sourceLines !! (n - 1))
4756+
| otherwise = Nothing
4757+
in
4758+
case (lineAt startLine, lineAt endLine) of
4759+
(Just startText, Just endText) ->
4760+
let startIndex = columnToIndex startText (fromIntegral $ posColumn start)
4761+
endIndex = columnToIndex endText (fromIntegral $ posColumn end)
4762+
in
4763+
if startLine == endLine
4764+
then
4765+
take (endIndex - startIndex) . drop startIndex $ startText
4766+
else
4767+
intercalate "\n" $
4768+
[drop startIndex startText]
4769+
++ [sourceLines !! (n - 1) | n <- [startLine + 1 .. endLine - 1]]
4770+
++ [take endIndex endText]
4771+
_ -> ""
4772+
4773+
columnToIndex line target = go line 0 1
4774+
where
4775+
go rest raw visual
4776+
| target <= visual = raw
4777+
go [] raw _ = raw
4778+
go ('\t':rest) raw visual = go rest (raw + 1) (visual + 8 - ((visual - 1) `mod` 8))
4779+
go (_:rest) raw visual = go rest (raw + 1) (visual + 1)
4780+
4781+
replaceSpan id start end r =
4782+
let depth = length $ getPath (parentMap params) (T_EOF id)
4783+
in
4784+
newReplacement {
4785+
repStartPos = start,
4786+
repEndPos = end,
4787+
repString = r,
4788+
repPrecedence = depth,
4789+
repInsertionPoint = InsertBefore
4790+
}
4791+
47114792
-- We don't tag operators like < and -o well enough to replace them,
47124793
-- so just handle the simple cases.
47134794
isSimple t = case t of

src/ShellCheck/AnalyzerLib.hs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ data Parameters = Parameters {
105105
rootNode :: Token,
106106
-- map from token id to start and end position
107107
tokenPositions :: Map.Map Id (Position, Position),
108+
-- original source text for source-sensitive fixes
109+
sourceText :: String,
108110
-- Result from Control Flow Graph analysis (including data flow analysis)
109111
cfgAnalysis :: Maybe CF.CFGAnalysis
110112
} deriving (Show)
@@ -138,7 +140,8 @@ defaultSpec pr = spec {
138140
asShellType = Nothing,
139141
asCheckSourced = False,
140142
asExecutionMode = Executed,
141-
asTokenPositions = prTokenPositions pr
143+
asTokenPositions = prTokenPositions pr,
144+
asSourceText = ""
142145
} where spec = newAnalysisSpec (fromJust $ prRoot pr)
143146

144147
pScript s =
@@ -154,7 +157,7 @@ producesComments :: Checker -> String -> Maybe Bool
154157
producesComments c s = do
155158
let pr = pScript s
156159
prRoot pr
157-
let spec = defaultSpec pr
160+
let spec = (defaultSpec pr) { asSourceText = s }
158161
let params = makeParameters spec
159162
return . not . null $ filterByAnnotation spec params $ runChecker params c
160163

@@ -237,6 +240,7 @@ makeParameters spec = params
237240
parentMap = getParentTree root,
238241
variableFlow = getVariableFlow params root,
239242
tokenPositions = asTokenPositions spec,
243+
sourceText = asSourceText spec,
240244
cfgAnalysis = do
241245
guard extendedAnalysis
242246
return $ CF.analyzeControlFlow cfParams root

src/ShellCheck/Checker.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ checkScript sys spec = do
8888
asCheckSourced = csCheckSourced spec,
8989
asExecutionMode = Executed,
9090
asTokenPositions = tokenPositions,
91+
asSourceText = contents,
9192
asExtendedAnalysis = csExtendedAnalysis spec,
9293
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
9394
} where as = newAnalysisSpec root

src/ShellCheck/Interface.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module ShellCheck.Interface
2525
, CheckResult(crFilename, crComments)
2626
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
2727
, ParseResult(prComments, prTokenPositions, prRoot)
28-
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
28+
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks, asSourceText)
2929
, AnalysisResult(arComments)
3030
, FormatterOptions(foColorOption, foWikiLinkCount)
3131
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
@@ -177,7 +177,8 @@ data AnalysisSpec = AnalysisSpec {
177177
asCheckSourced :: Bool,
178178
asOptionalChecks :: [String],
179179
asExtendedAnalysis :: Maybe Bool,
180-
asTokenPositions :: Map.Map Id (Position, Position)
180+
asTokenPositions :: Map.Map Id (Position, Position),
181+
asSourceText :: String
181182
}
182183

183184
newAnalysisSpec token = AnalysisSpec {
@@ -188,7 +189,8 @@ newAnalysisSpec token = AnalysisSpec {
188189
asCheckSourced = False,
189190
asOptionalChecks = [],
190191
asExtendedAnalysis = Nothing,
191-
asTokenPositions = Map.empty
192+
asTokenPositions = Map.empty,
193+
asSourceText = ""
192194
}
193195

194196
newtype AnalysisResult = AnalysisResult {

0 commit comments

Comments
 (0)