packages feed

ShellCheck 0.8.0 → 0.9.0

raw patch · 27 files changed

+4415/−810 lines, 27 filesdep +fgldep +transformersdep ~Diffdep ~QuickCheckdep ~aesonPVP ok

version bump matches the API change (PVP)

Dependencies added: fgl, transformers

Dependency ranges changed: Diff, QuickCheck, aeson, array, base, bytestring, containers, deepseq, directory, filepath, mtl, parsec, regex-tdfa

API changes (from Hackage documentation)

- ShellCheck.Analytics: instance GHC.Base.Monoid ShellCheck.Analytics.SpaceStatus
- ShellCheck.Analytics: instance GHC.Base.Semigroup ShellCheck.Analytics.SpaceStatus
- ShellCheck.Analytics: instance GHC.Classes.Eq ShellCheck.Analytics.SpaceStatus
- ShellCheck.Analytics: runAnalytics :: AnalysisSpec -> [TokenComment]
- ShellCheck.AnalyzerLib: (!!!) :: [a] -> Int -> Maybe a
- ShellCheck.AnalyzerLib: containsInheritErrexit :: Token -> Bool
- ShellCheck.AnalyzerLib: containsLastpipe :: Token -> Bool
- ShellCheck.AnalyzerLib: containsPipefail :: Token -> Bool
- ShellCheck.AnalyzerLib: getBracedModifier :: [Char] -> [Char]
- ShellCheck.AnalyzerLib: getBracedReference :: [Char] -> [Char]
- ShellCheck.AnalyzerLib: getIndexReferences :: String -> [String]
- ShellCheck.AnalyzerLib: getOffsetReferences :: String -> [String]
- ShellCheck.AnalyzerLib: getPath :: Map Id Token -> Token -> [Token]
- ShellCheck.AnalyzerLib: headOrDefault :: p -> [p] -> p
- ShellCheck.AnalyzerLib: isClosingFileOp :: Token -> Bool
- ShellCheck.AnalyzerLib: isSpecialVariableChar :: Char -> Bool
- ShellCheck.AnalyzerLib: isUnmodifiedParameterExpansion :: Token -> Bool
- ShellCheck.AnalyzerLib: isVariableChar :: Char -> Bool
- ShellCheck.AnalyzerLib: isVariableName :: [Char] -> Bool
- ShellCheck.AnalyzerLib: isVariableStartChar :: Char -> Bool
- ShellCheck.AnalyzerLib: lastOrDefault :: p -> [p] -> p
- ShellCheck.AnalyzerLib: prop_getBracedModifier1 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedModifier2 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedModifier3 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedModifier4 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedModifier5 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference1 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference10 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference11 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference11b :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference12 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference13 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference2 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference3 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference4 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference5 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference6 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference7 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference8 :: Bool
- ShellCheck.AnalyzerLib: prop_getBracedReference9 :: Bool
- ShellCheck.AnalyzerLib: prop_getOffsetReferences1 :: Bool
- ShellCheck.AnalyzerLib: prop_getOffsetReferences2 :: Bool
- ShellCheck.AnalyzerLib: prop_getOffsetReferences3 :: Bool
- ShellCheck.AnalyzerLib: prop_getOffsetReferences4 :: Bool
- ShellCheck.AnalyzerLib: prop_isVariableName1 :: Bool
- ShellCheck.AnalyzerLib: prop_isVariableName2 :: Bool
- ShellCheck.AnalyzerLib: prop_isVariableName3 :: Bool
- ShellCheck.AnalyzerLib: variableNameRegex :: Regex
+ ShellCheck.AST: Inner_TA_Parenthesis :: t -> InnerToken t
+ ShellCheck.AST: pattern TA_Parentesis :: Id -> Token -> Token
+ ShellCheck.ASTLib: getBracedModifier :: [Char] -> String
+ ShellCheck.ASTLib: getBracedReference :: [Char] -> [Char]
+ ShellCheck.ASTLib: getEnableDirectives :: Token -> [String]
+ ShellCheck.ASTLib: getIndexReferences :: String -> [String]
+ ShellCheck.ASTLib: getOffsetReferences :: String -> [String]
+ ShellCheck.ASTLib: getPath :: Map Id Token -> Token -> [Token]
+ ShellCheck.ASTLib: isClosingFileOp :: Token -> Bool
+ ShellCheck.ASTLib: isSpecialVariableChar :: Char -> Bool
+ ShellCheck.ASTLib: isUnmodifiedParameterExpansion :: Token -> Bool
+ ShellCheck.ASTLib: isVariableChar :: Char -> Bool
+ ShellCheck.ASTLib: isVariableName :: [Char] -> Bool
+ ShellCheck.ASTLib: isVariableStartChar :: Char -> Bool
+ ShellCheck.ASTLib: prop_getBracedModifier1 :: Bool
+ ShellCheck.ASTLib: prop_getBracedModifier2 :: Bool
+ ShellCheck.ASTLib: prop_getBracedModifier3 :: Bool
+ ShellCheck.ASTLib: prop_getBracedModifier4 :: Bool
+ ShellCheck.ASTLib: prop_getBracedModifier5 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference1 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference10 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference11 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference11b :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference12 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference13 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference2 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference3 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference4 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference5 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference6 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference7 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference8 :: Bool
+ ShellCheck.ASTLib: prop_getBracedReference9 :: Bool
+ ShellCheck.ASTLib: prop_getIndexReferences1 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString1 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString10 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString11 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString12 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString13 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString2 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString3 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString4 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString5 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString6 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString7 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString8 :: Bool
+ ShellCheck.ASTLib: prop_getLiteralString9 :: Bool
+ ShellCheck.ASTLib: prop_getOffsetReferences1 :: Bool
+ ShellCheck.ASTLib: prop_getOffsetReferences2 :: Bool
+ ShellCheck.ASTLib: prop_getOffsetReferences3 :: Bool
+ ShellCheck.ASTLib: prop_getOffsetReferences4 :: Bool
+ ShellCheck.ASTLib: prop_isVariableName1 :: Bool
+ ShellCheck.ASTLib: prop_isVariableName2 :: Bool
+ ShellCheck.ASTLib: prop_isVariableName3 :: Bool
+ ShellCheck.ASTLib: variableNameRegex :: Regex
+ ShellCheck.Analytics: checker :: AnalysisSpec -> Parameters -> Checker
+ ShellCheck.AnalyzerLib: [cfgAnalysis] :: Parameters -> CFGAnalysis
+ ShellCheck.AnalyzerLib: [idMap] :: Parameters -> Map Id Token
+ ShellCheck.AnalyzerLib: containsSetOption :: [Char] -> Token -> Bool
+ ShellCheck.AnalyzerLib: isOptionSet :: [Char] -> Token -> Bool
+ ShellCheck.CFG: CFApplyEffects :: [IdTagged CFEffect] -> CFNode
+ ShellCheck.CFG: CFDefineFunction :: String -> Id -> Node -> Node -> CFEffect
+ ShellCheck.CFG: CFDropPrefixAssignments :: CFNode
+ ShellCheck.CFG: CFEErrExit :: CFEdge
+ ShellCheck.CFG: CFEExit :: CFEdge
+ ShellCheck.CFG: CFEFalseFlow :: CFEdge
+ ShellCheck.CFG: CFEFlow :: CFEdge
+ ShellCheck.CFG: CFEntryPoint :: String -> CFNode
+ ShellCheck.CFG: CFExecuteCommand :: Maybe String -> CFNode
+ ShellCheck.CFG: CFExecuteSubshell :: String -> Node -> Node -> CFNode
+ ShellCheck.CFG: CFGParameters :: Bool -> Bool -> CFGParameters
+ ShellCheck.CFG: CFGResult :: CFGraph -> Map Id (Node, Node) -> Map Id (Set Node) -> Array Node [Node] -> CFGResult
+ ShellCheck.CFG: CFHintArray :: String -> CFEffect
+ ShellCheck.CFG: CFHintDefined :: String -> CFEffect
+ ShellCheck.CFG: CFImpliedExit :: CFNode
+ ShellCheck.CFG: CFReadVariable :: String -> CFEffect
+ ShellCheck.CFG: CFResolvedExit :: CFNode
+ ShellCheck.CFG: CFSetBackgroundPid :: Id -> CFNode
+ ShellCheck.CFG: CFSetExitCode :: Id -> CFNode
+ ShellCheck.CFG: CFSetProps :: Scope -> String -> Set CFVariableProp -> CFEffect
+ ShellCheck.CFG: CFStringInteger :: CFStringPart
+ ShellCheck.CFG: CFStringLiteral :: String -> CFStringPart
+ ShellCheck.CFG: CFStringUnknown :: CFStringPart
+ ShellCheck.CFG: CFStringVariable :: String -> CFStringPart
+ ShellCheck.CFG: CFStructuralNode :: CFNode
+ ShellCheck.CFG: CFUndefine :: String -> CFEffect
+ ShellCheck.CFG: CFUndefineFunction :: String -> CFEffect
+ ShellCheck.CFG: CFUndefineNameref :: String -> CFEffect
+ ShellCheck.CFG: CFUndefineVariable :: String -> CFEffect
+ ShellCheck.CFG: CFUnreachable :: CFNode
+ ShellCheck.CFG: CFUnresolvedExit :: CFNode
+ ShellCheck.CFG: CFUnsetProps :: Scope -> String -> Set CFVariableProp -> CFEffect
+ ShellCheck.CFG: CFVPArray :: CFVariableProp
+ ShellCheck.CFG: CFVPAssociative :: CFVariableProp
+ ShellCheck.CFG: CFVPExport :: CFVariableProp
+ ShellCheck.CFG: CFVPInteger :: CFVariableProp
+ ShellCheck.CFG: CFValueArray :: CFValue
+ ShellCheck.CFG: CFValueComputed :: Id -> [CFStringPart] -> CFValue
+ ShellCheck.CFG: CFValueInteger :: CFValue
+ ShellCheck.CFG: CFValueString :: CFValue
+ ShellCheck.CFG: CFValueUninitialized :: CFValue
+ ShellCheck.CFG: CFWriteGlobal :: String -> CFValue -> CFEffect
+ ShellCheck.CFG: CFWriteLocal :: String -> CFValue -> CFEffect
+ ShellCheck.CFG: CFWritePrefix :: String -> CFValue -> CFEffect
+ ShellCheck.CFG: CFWriteVariable :: String -> CFValue -> CFEffect
+ ShellCheck.CFG: DefaultScope :: Scope
+ ShellCheck.CFG: GlobalScope :: Scope
+ ShellCheck.CFG: IdTagged :: Id -> a -> IdTagged a
+ ShellCheck.CFG: LocalScope :: Scope
+ ShellCheck.CFG: PrefixScope :: Scope
+ ShellCheck.CFG: [cfGraph] :: CFGResult -> CFGraph
+ ShellCheck.CFG: [cfIdToNodes] :: CFGResult -> Map Id (Set Node)
+ ShellCheck.CFG: [cfIdToRange] :: CFGResult -> Map Id (Node, Node)
+ ShellCheck.CFG: [cfLastpipe] :: CFGParameters -> Bool
+ ShellCheck.CFG: [cfPipefail] :: CFGParameters -> Bool
+ ShellCheck.CFG: [cfPostDominators] :: CFGResult -> Array Node [Node]
+ ShellCheck.CFG: buildGraph :: CFGParameters -> Token -> CFGResult
+ ShellCheck.CFG: data CFEdge
+ ShellCheck.CFG: data CFEffect
+ ShellCheck.CFG: data CFGParameters
+ ShellCheck.CFG: data CFGResult
+ ShellCheck.CFG: data CFNode
+ ShellCheck.CFG: data CFStringPart
+ ShellCheck.CFG: data CFValue
+ ShellCheck.CFG: data CFVariableProp
+ ShellCheck.CFG: data IdTagged a
+ ShellCheck.CFG: data Scope
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.CFEdge
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.CFEffect
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.CFNode
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.CFStringPart
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.CFValue
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.CFVariableProp
+ ShellCheck.CFG: instance Control.DeepSeq.NFData ShellCheck.CFG.Scope
+ ShellCheck.CFG: instance Control.DeepSeq.NFData a => Control.DeepSeq.NFData (ShellCheck.CFG.IdTagged a)
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.CFEdge
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.CFEffect
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.CFNode
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.CFStringPart
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.CFValue
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.CFVariableProp
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.Range
+ ShellCheck.CFG: instance GHC.Classes.Eq ShellCheck.CFG.Scope
+ ShellCheck.CFG: instance GHC.Classes.Eq a => GHC.Classes.Eq (ShellCheck.CFG.IdTagged a)
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.CFEdge
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.CFEffect
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.CFNode
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.CFStringPart
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.CFValue
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.CFVariableProp
+ ShellCheck.CFG: instance GHC.Classes.Ord ShellCheck.CFG.Scope
+ ShellCheck.CFG: instance GHC.Classes.Ord a => GHC.Classes.Ord (ShellCheck.CFG.IdTagged a)
+ ShellCheck.CFG: instance GHC.Generics.Generic (ShellCheck.CFG.IdTagged a)
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.CFEdge
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.CFEffect
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.CFNode
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.CFStringPart
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.CFValue
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.CFVariableProp
+ ShellCheck.CFG: instance GHC.Generics.Generic ShellCheck.CFG.Scope
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFEdge
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFEffect
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFGResult
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFNode
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFStringPart
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFValue
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.CFVariableProp
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.Range
+ ShellCheck.CFG: instance GHC.Show.Show ShellCheck.CFG.Scope
+ ShellCheck.CFG: instance GHC.Show.Show a => GHC.Show.Show (ShellCheck.CFG.IdTagged a)
+ ShellCheck.CFG: runTests :: IO Bool
+ ShellCheck.CFG: type CFGraph = Gr CFNode CFEdge
+ ShellCheck.CFGAnalysis: CFGAnalysis :: CFGraph -> Map Id (Node, Node) -> Map Id (Set Node) -> Array Node [Node] -> Map Node (ProgramState, ProgramState) -> CFGAnalysis
+ ShellCheck.CFGAnalysis: CFGParameters :: Bool -> Bool -> CFGParameters
+ ShellCheck.CFGAnalysis: NumericalStatusDefinitely :: NumericalStatus
+ ShellCheck.CFGAnalysis: NumericalStatusEmpty :: NumericalStatus
+ ShellCheck.CFGAnalysis: NumericalStatusMaybe :: NumericalStatus
+ ShellCheck.CFGAnalysis: NumericalStatusUnknown :: NumericalStatus
+ ShellCheck.CFGAnalysis: ProgramState :: Map String VariableState -> Set Id -> Bool -> ProgramState
+ ShellCheck.CFGAnalysis: SpaceStatusClean :: SpaceStatus
+ ShellCheck.CFGAnalysis: SpaceStatusDirty :: SpaceStatus
+ ShellCheck.CFGAnalysis: SpaceStatusEmpty :: SpaceStatus
+ ShellCheck.CFGAnalysis: VariableState :: VariableValue -> VariableProperties -> VariableState
+ ShellCheck.CFGAnalysis: VariableValue :: Maybe String -> SpaceStatus -> NumericalStatus -> VariableValue
+ ShellCheck.CFGAnalysis: [cfLastpipe] :: CFGParameters -> Bool
+ ShellCheck.CFGAnalysis: [cfPipefail] :: CFGParameters -> Bool
+ ShellCheck.CFGAnalysis: [exitCodes] :: ProgramState -> Set Id
+ ShellCheck.CFGAnalysis: [graph] :: CFGAnalysis -> CFGraph
+ ShellCheck.CFGAnalysis: [literalValue] :: VariableValue -> Maybe String
+ ShellCheck.CFGAnalysis: [nodeToData] :: CFGAnalysis -> Map Node (ProgramState, ProgramState)
+ ShellCheck.CFGAnalysis: [numericalStatus] :: VariableValue -> NumericalStatus
+ ShellCheck.CFGAnalysis: [postDominators] :: CFGAnalysis -> Array Node [Node]
+ ShellCheck.CFGAnalysis: [spaceStatus] :: VariableValue -> SpaceStatus
+ ShellCheck.CFGAnalysis: [stateIsReachable] :: ProgramState -> Bool
+ ShellCheck.CFGAnalysis: [tokenToNodes] :: CFGAnalysis -> Map Id (Set Node)
+ ShellCheck.CFGAnalysis: [tokenToRange] :: CFGAnalysis -> Map Id (Node, Node)
+ ShellCheck.CFGAnalysis: [variableProperties] :: VariableState -> VariableProperties
+ ShellCheck.CFGAnalysis: [variableValue] :: VariableState -> VariableValue
+ ShellCheck.CFGAnalysis: [variablesInScope] :: ProgramState -> Map String VariableState
+ ShellCheck.CFGAnalysis: analyzeControlFlow :: CFGParameters -> Token -> CFGAnalysis
+ ShellCheck.CFGAnalysis: data CFGAnalysis
+ ShellCheck.CFGAnalysis: data CFGParameters
+ ShellCheck.CFGAnalysis: data NumericalStatus
+ ShellCheck.CFGAnalysis: data ProgramState
+ ShellCheck.CFGAnalysis: data SpaceStatus
+ ShellCheck.CFGAnalysis: data VariableState
+ ShellCheck.CFGAnalysis: data VariableValue
+ ShellCheck.CFGAnalysis: doesPostDominate :: CFGAnalysis -> Id -> Id -> Bool
+ ShellCheck.CFGAnalysis: getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState
+ ShellCheck.CFGAnalysis: getOutgoingState :: CFGAnalysis -> Id -> Maybe ProgramState
+ ShellCheck.CFGAnalysis: instance (Control.DeepSeq.NFData k, Control.DeepSeq.NFData v) => Control.DeepSeq.NFData (ShellCheck.CFGAnalysis.VersionedMap k v)
+ ShellCheck.CFGAnalysis: instance (GHC.Classes.Eq k, GHC.Classes.Eq v) => GHC.Classes.Eq (ShellCheck.CFGAnalysis.VersionedMap k v)
+ ShellCheck.CFGAnalysis: instance (GHC.Classes.Ord k, GHC.Classes.Ord v) => GHC.Classes.Ord (ShellCheck.CFGAnalysis.VersionedMap k v)
+ ShellCheck.CFGAnalysis: instance (GHC.Show.Show k, GHC.Show.Show v) => GHC.Show.Show (ShellCheck.CFGAnalysis.VersionedMap k v)
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData (ShellCheck.CFGAnalysis.StackEntry s)
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.FunctionDefinition
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.InternalState
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.NumericalStatus
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.ProgramState
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.SpaceStatus
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.StateDependency
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.VariableState
+ ShellCheck.CFGAnalysis: instance Control.DeepSeq.NFData ShellCheck.CFGAnalysis.VariableValue
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq (ShellCheck.CFGAnalysis.StackEntry s)
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.FunctionDefinition
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.InternalState
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.NumericalStatus
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.ProgramState
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.SpaceStatus
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.StateDependency
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.VariableState
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Eq ShellCheck.CFGAnalysis.VariableValue
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Ord ShellCheck.CFGAnalysis.FunctionDefinition
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Ord ShellCheck.CFGAnalysis.NumericalStatus
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Ord ShellCheck.CFGAnalysis.SpaceStatus
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Ord ShellCheck.CFGAnalysis.StateDependency
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Ord ShellCheck.CFGAnalysis.VariableState
+ ShellCheck.CFGAnalysis: instance GHC.Classes.Ord ShellCheck.CFGAnalysis.VariableValue
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic (ShellCheck.CFGAnalysis.StackEntry s)
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic (ShellCheck.CFGAnalysis.VersionedMap k v)
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.FunctionDefinition
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.InternalState
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.NumericalStatus
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.ProgramState
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.SpaceStatus
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.StateDependency
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.VariableState
+ ShellCheck.CFGAnalysis: instance GHC.Generics.Generic ShellCheck.CFGAnalysis.VariableValue
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.CFGAnalysis
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.FunctionDefinition
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.InternalState
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.NumericalStatus
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.ProgramState
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.SpaceStatus
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.StateDependency
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.VariableState
+ ShellCheck.CFGAnalysis: instance GHC.Show.Show ShellCheck.CFGAnalysis.VariableValue
+ ShellCheck.CFGAnalysis: runTests :: IO Bool
+ ShellCheck.CFGAnalysis: type VariableProperties = Set (Set CFVariableProp)
+ ShellCheck.Checks.ControlFlow: checker :: AnalysisSpec -> Parameters -> Checker
+ ShellCheck.Checks.ControlFlow: optionalChecks :: [CheckDescription]
+ ShellCheck.Checks.ControlFlow: runTests :: IO Bool
+ ShellCheck.Data: flagsForMapfile :: String
+ ShellCheck.Data: specialIntegerVariables :: [String]
+ ShellCheck.Prelude: (!!!) :: [a] -> Int -> Maybe a
+ ShellCheck.Prelude: headOrDefault :: a -> [a] -> a
+ ShellCheck.Prelude: lastOrDefault :: p -> [p] -> p
+ ShellCheck.Prelude: pleaseReport :: [Char] -> [Char]
+ ShellCheck.Prelude: sconcat1 :: Semigroup t => [t] -> t
+ ShellCheck.Prelude: sconcatOrDefault :: Semigroup p => p -> [p] -> p
- ShellCheck.AST: Inner_T_BatsTest :: t -> t -> InnerToken t
+ ShellCheck.AST: Inner_T_BatsTest :: String -> t -> InnerToken t
- ShellCheck.AST: pattern T_BatsTest :: Id -> Token -> Token -> Token
+ ShellCheck.AST: pattern T_BatsTest :: Id -> String -> Token -> Token
- ShellCheck.AnalyzerLib: Parameters :: Bool -> Bool -> Bool -> Bool -> [StackData] -> Map Id Token -> Shell -> Bool -> Token -> Map Id (Position, Position) -> Parameters
+ ShellCheck.AnalyzerLib: Parameters :: Bool -> Bool -> Bool -> Bool -> [StackData] -> Map Id Token -> Map Id Token -> Shell -> Bool -> Token -> Map Id (Position, Position) -> CFGAnalysis -> Parameters
- ShellCheck.AnalyzerLib: determineShellTest :: [Char] -> Shell
+ ShellCheck.AnalyzerLib: determineShellTest :: String -> Shell
- ShellCheck.AnalyzerLib: determineShellTest' :: Maybe Shell -> [Char] -> Shell
+ ShellCheck.AnalyzerLib: determineShellTest' :: Maybe Shell -> String -> Shell
- ShellCheck.AnalyzerLib: isDereferencingBinaryOp :: [Char] -> Bool
+ ShellCheck.AnalyzerLib: isDereferencingBinaryOp :: String -> Bool
- ShellCheck.AnalyzerLib: modifiesVariable :: Parameters -> Token -> [Char] -> Bool
+ ShellCheck.AnalyzerLib: modifiesVariable :: Parameters -> Token -> String -> Bool
- ShellCheck.AnalyzerLib: pScript :: [Char] -> ParseResult
+ ShellCheck.AnalyzerLib: pScript :: String -> ParseResult
- ShellCheck.Data: arithmeticBinaryTestOps :: [[Char]]
+ ShellCheck.Data: arithmeticBinaryTestOps :: [String]
- ShellCheck.Data: arrayVariables :: [[Char]]
+ ShellCheck.Data: arrayVariables :: [String]
- ShellCheck.Data: binaryTestOps :: [[Char]]
+ ShellCheck.Data: binaryTestOps :: [String]
- ShellCheck.Data: commonCommands :: [[Char]]
+ ShellCheck.Data: commonCommands :: [String]
- ShellCheck.Data: declaringCommands :: [[Char]]
+ ShellCheck.Data: declaringCommands :: [String]
- ShellCheck.Data: flagsForRead :: [Char]
+ ShellCheck.Data: flagsForRead :: String
- ShellCheck.Data: internalVariables :: [[Char]]
+ ShellCheck.Data: internalVariables :: [String]
- ShellCheck.Data: nonReadingCommands :: [[Char]]
+ ShellCheck.Data: nonReadingCommands :: [String]
- ShellCheck.Data: sampleWords :: [[Char]]
+ ShellCheck.Data: sampleWords :: [String]
- ShellCheck.Data: specialVariables :: [[Char]]
+ ShellCheck.Data: specialVariables :: [String]
- ShellCheck.Data: specialVariablesWithoutSpaces :: [[Char]]
+ ShellCheck.Data: specialVariablesWithoutSpaces :: [String]
- ShellCheck.Data: unaryTestOps :: [[Char]]
+ ShellCheck.Data: unaryTestOps :: [String]
- ShellCheck.Data: unbracedVariables :: [[Char]]
+ ShellCheck.Data: unbracedVariables :: [String]
- ShellCheck.Data: variablesWithoutSpaces :: [[Char]]
+ ShellCheck.Data: variablesWithoutSpaces :: [String]
- ShellCheck.Formatter.Format: makeNonVirtual :: [PositionedComment] -> [Char] -> [PositionedComment]
+ ShellCheck.Formatter.Format: makeNonVirtual :: [PositionedComment] -> String -> [PositionedComment]
- ShellCheck.Interface: mockRcFile :: forall (m :: Type -> Type). Monad m => String -> SystemInterface m -> SystemInterface m
+ ShellCheck.Interface: mockRcFile :: forall {m :: Type -> Type}. Monad m => String -> SystemInterface m -> SystemInterface m

Files

CHANGELOG.md view
@@ -1,3 +1,25 @@+## v0.9.0 - 2022-12-12+### Added+- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)+- SC2317: Warn about unreachable commands+- SC2318: Warn about backreferences in 'declare x=1 y=$x'+- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test+- SC2321: Suggest removing $((..)) in array[$((idx))]=val+- SC2322: Suggest collapsing double parentheses in arithmetic contexts+- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val++### Fixed+- SC2086: Now uses DFA to make more accurate predictions about values+- SC2086: No longer warns about values declared as integer with declare -i++### Changed+- ShellCheck now has a Data Flow Analysis engine to make smarter decisions+  based on control flow rather than just syntax. Existing checks will+  gradually start using it, which may cause them to trigger differently+  (but more accurately).+- Values in directives/shellcheckrc can now be quoted with '' or ""++ ## v0.8.0 - 2021-11-06 ### Added - `disable=all` now conveniently disables all warnings
LICENSE view
@@ -1,13 +1,3 @@-Employer mandated disclaimer:--  I am providing code in the repository to you under an open source license.-  Because this is my personal repository, the license you receive to my code is-  from me and other individual contributors, and not my employer (Facebook).--  - Vidar "koala_man" Holen-------                     GNU GENERAL PUBLIC LICENSE                        Version 3, 29 June 2007 
ShellCheck.cabal view
@@ -1,5 +1,5 @@ Name:             ShellCheck-Version:          0.8.0+Version:          0.9.0 Synopsis:         Shell script analysis tool License:          GPL-3 License-file:     LICENSE@@ -45,19 +45,26 @@       build-depends:         semigroups     build-depends:-      aeson,-      array,-      base >= 4.8.0.0 && < 5,-      bytestring,-      containers >= 0.5,-      deepseq >= 1.4.0.0,-      Diff >= 0.2.0,-      directory >= 1.2.3.0,-      mtl >= 2.2.1,-      filepath,-      parsec,-      regex-tdfa,-      QuickCheck >= 2.7.4,+      -- The lower bounds are based on GHC 7.10.3+      -- The upper bounds are based on GHC 9.4.3+      aeson                >= 1.4.0 && < 2.2,+      array                >= 0.5.1 && < 0.6,+      base                 >= 4.8.0.0 && < 5,+      bytestring           >= 0.10.6 && < 0.12,+      containers           >= 0.5.6 && < 0.7,+      deepseq              >= 1.4.1 && < 1.5,+      Diff                 >= 0.4.0 && < 0.5,+      fgl                  >= 5.7.0 && < 5.9,+      filepath             >= 1.4.0 && < 1.5,+      mtl                  >= 2.2.2 && < 2.3,+      parsec               >= 3.1.14 && < 3.2,+      QuickCheck           >= 2.14.2 && < 2.15,+      regex-tdfa           >= 1.2.0 && < 1.4,+      transformers         >= 0.4.2 && < 0.6,++      -- getXdgDirectory from 1.2.3.0+      directory            >= 1.2.3 && < 1.4,+       -- When cabal supports it, move this to setup-depends:       process     exposed-modules:@@ -66,11 +73,15 @@       ShellCheck.Analytics       ShellCheck.Analyzer       ShellCheck.AnalyzerLib+      ShellCheck.CFG+      ShellCheck.CFGAnalysis       ShellCheck.Checker       ShellCheck.Checks.Commands+      ShellCheck.Checks.ControlFlow       ShellCheck.Checks.Custom       ShellCheck.Checks.ShellSupport       ShellCheck.Data+      ShellCheck.Debug       ShellCheck.Fixer       ShellCheck.Formatter.Format       ShellCheck.Formatter.CheckStyle@@ -82,6 +93,7 @@       ShellCheck.Formatter.Quiet       ShellCheck.Interface       ShellCheck.Parser+      ShellCheck.Prelude       ShellCheck.Regex     other-modules:       Paths_ShellCheck@@ -94,17 +106,19 @@     build-depends:       aeson,       array,-      base >= 4 && < 5,+      base,       bytestring,       containers,-      deepseq >= 1.4.0.0,-      Diff >= 0.2.0,-      directory >= 1.2.3.0,-      mtl >= 2.2.1,+      deepseq,+      Diff,+      directory,+      fgl,+      mtl,       filepath,-      parsec >= 3.0,-      QuickCheck >= 2.7.4,+      parsec,+      QuickCheck,       regex-tdfa,+      transformers,       ShellCheck     default-language: Haskell98     main-is: shellcheck.hs@@ -114,17 +128,19 @@     build-depends:       aeson,       array,-      base >= 4 && < 5,+      base,       bytestring,       containers,-      deepseq >= 1.4.0.0,-      Diff >= 0.2.0,-      directory >= 1.2.3.0,-      mtl >= 2.2.1,+      deepseq,+      Diff,+      directory,+      fgl,       filepath,+      mtl,       parsec,-      QuickCheck >= 2.7.4,+      QuickCheck,       regex-tdfa,+      transformers,       ShellCheck     default-language: Haskell98     main-is: test/shellcheck.hs
shellcheck.1.md view
@@ -282,6 +282,9 @@     source-path=SCRIPTDIR     source-path=/mnt/chroot +    # Since 0.9.0, values can be quoted with '' or "" to allow spaces+    source-path="My Documents/scripts"+     # Allow opening any 'source'd file, even if not specified as input     external-sources=true @@ -375,7 +378,7 @@  # COPYRIGHT -Copyright 2012-2021, Vidar Holen and contributors.+Copyright 2012-2022, Vidar Holen and contributors. Licensed under the GNU General Public License version 3 or later, see https://gnu.org/licenses/gpl.html 
shellcheck.hs view
@@ -34,6 +34,8 @@  import           Control.Exception import           Control.Monad+import           Control.Monad.IO.Class+import           Control.Monad.Trans.Class import           Control.Monad.Except import           Data.Bits import           Data.Char@@ -225,7 +227,7 @@     f :: Status -> FilePath -> IO Status     f status file = do         newStatus <- process file `catch` handler file-        return $ status `mappend` newStatus+        return $! status `mappend` newStatus     handler :: FilePath -> IOException -> IO Status     handler file e = reportFailure file (show e)     reportFailure file str = do
src/ShellCheck/AST.hs view
@@ -45,6 +45,7 @@     | Inner_TA_Variable String [t]     | Inner_TA_Expansion [t]     | Inner_TA_Sequence [t]+    | Inner_TA_Parenthesis t     | Inner_TA_Trinary t t t     | Inner_TA_Unary String t     | Inner_TC_And ConditionType String t t@@ -141,7 +142,7 @@     | Inner_T_CoProcBody t     | Inner_T_Include t     | Inner_T_SourceCommand t t-    | Inner_T_BatsTest t t+    | Inner_T_BatsTest String t     deriving (Show, Eq, Functor, Foldable, Traversable)  data Annotation =@@ -204,6 +205,7 @@ pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)+pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t) pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)@@ -256,7 +258,7 @@ pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) -{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}+{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}  instance Eq Token where     OuterToken _ a == OuterToken _ b = a == b
src/ShellCheck/ASTLib.hs view
@@ -21,6 +21,7 @@ module ShellCheck.ASTLib where  import ShellCheck.AST+import ShellCheck.Prelude import ShellCheck.Regex  import Control.Monad.Writer@@ -138,7 +139,7 @@     flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]     flag (x, '-':args) = map (\v -> (x, [v])) args     flag (x, _) = [ (x, "") ]-getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"+getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"  -- Get all flags in a GNU way, up until -- getAllFlags :: Token -> [(Token, String)]@@ -369,6 +370,21 @@     f (T_Glob _ str) = return str     f _ = Nothing ++prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"+prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"+prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"+prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"+prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"+prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"+prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"+prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"+prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"+prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"+prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"+prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"+prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"+ -- Maybe get the literal value of a token, using a custom function -- to map unrecognized Tokens into strings. getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String@@ -401,14 +417,15 @@             '\\' -> '\\' : rest             'x' ->                 case cs of-                    (x:y:more) ->-                        if isHexDigit x && isHexDigit y-                        then chr (16*(digitToInt x) + (digitToInt y)) : rest-                        else '\\':c:rest+                    (x:y:more) | isHexDigit x && isHexDigit y ->+                        chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more+                    (x:more) | isHexDigit x ->+                        chr (digitToInt x) : decodeEscapes more+                    more -> '\\' : 'x' : decodeEscapes more             _ | isOctDigit c ->-                let digits = take 3 $ takeWhile isOctDigit (c:cs)-                    num = parseOct digits-                in (if num < 256 then chr num else '?') : rest+                let (digits, more) = spanMax isOctDigit 3 (c:cs)+                    num = (parseOct digits) `mod` 256+                in (chr num) : decodeEscapes more             _ -> '\\' : c : rest       where         rest = decodeEscapes cs@@ -416,6 +433,11 @@           where             f n "" = n             f n (c:rest) = f (n * 8 + digitToInt c) rest+        spanMax f n list =+            let (first, second) = span f list+                (prefix, suffix) = splitAt n first+            in+                (prefix, suffix ++ second)     decodeEscapes (c:cs) = c : decodeEscapes cs     decodeEscapes [] = [] @@ -763,6 +785,123 @@     fromEnvArgs args = fromMaybe "" $ find (notElem '=') $ skipFlags args     basename s = reverse . takeWhile (/= '/') . reverse $ s     skipFlags = dropWhile ("-" `isPrefixOf`)+++-- Determining if a name is a variable+isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x+isVariableChar x = isVariableStartChar x || isDigit x+isSpecialVariableChar = (`elem` "*@#?-$!")+variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"++prop_isVariableName1 = isVariableName "_fo123"+prop_isVariableName2 = not $ isVariableName "4"+prop_isVariableName3 = not $ isVariableName "test: "+isVariableName (x:r) = isVariableStartChar x && all isVariableChar r+isVariableName _     = False+++-- Get the variable name from an expansion like ${var:-foo}+prop_getBracedReference1 = getBracedReference "foo" == "foo"+prop_getBracedReference2 = getBracedReference "#foo" == "foo"+prop_getBracedReference3 = getBracedReference "#" == "#"+prop_getBracedReference4 = getBracedReference "##" == "#"+prop_getBracedReference5 = getBracedReference "#!" == "!"+prop_getBracedReference6 = getBracedReference "!#" == "#"+prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"+prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"+prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"+prop_getBracedReference10 = getBracedReference "foo: -1" == "foo"+prop_getBracedReference11 = getBracedReference "!os*" == ""+prop_getBracedReference11b = getBracedReference "!os@" == ""+prop_getBracedReference12 = getBracedReference "!os?bar**" == ""+prop_getBracedReference13 = getBracedReference "foo[bar]" == "foo"+getBracedReference s = fromMaybe s $+    nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s+  where+    noPrefix = dropPrefix s+    dropPrefix (c:rest) | c `elem` "!#" = rest+    dropPrefix cs = cs+    takeName s = do+        let name = takeWhile isVariableChar s+        guard . not $ null name+        return name+    getSpecial (c:_) | isSpecialVariableChar c = return [c]+    getSpecial _ = fail "empty or not special"++    nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}+        guard $ isVariableChar next -- e.g. ${!@}+        first <- find (not . isVariableChar) rest+        guard $ first `elem` "*?@"+        return ""+    nameExpansion _ = Nothing++-- Get the variable modifier like /a/b in ${var/a/b}+prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"+prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"+prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"+prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"+prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"+getBracedModifier s = headOrDefault "" $ do+    let var = getBracedReference s+    a <- dropModifier s+    dropPrefix var a+  where+    dropPrefix [] t        = return t+    dropPrefix (a:b) (c:d) | a == c = dropPrefix b d+    dropPrefix _ _         = []++    dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]+    dropModifier x        = [x]++-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}+prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]+getIndexReferences s = fromMaybe [] $ do+    match <- matchRegex re s+    index <- match !!! 0+    return $ matchAllStrings variableNameRegex index+  where+    re = mkRegex "(\\[.*\\])"++prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]+prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]+prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]+prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]+getOffsetReferences mods = fromMaybe [] $ do+-- if mods start with [, then drop until ]+    match <- matchRegex re mods+    offsets <- match !!! 1+    return $ matchAllStrings variableNameRegex offsets+  where+    re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"+++-- Returns whether a token is a parameter expansion without any modifiers.+-- True for $var ${var} $1 $#+-- False for ${#var} ${var[x]} ${var:-0}+isUnmodifiedParameterExpansion t =+    case t of+        T_DollarBraced _ False _ -> True+        T_DollarBraced _ _ list ->+            let str = concat $ oversimplify list+            in getBracedReference str == str+        _ -> False++--- A list of the element and all its parents up to the root node.+getPath tree t = t :+    case Map.lookup (getId t) tree of+        Nothing     -> []+        Just parent -> getPath tree parent++isClosingFileOp op =+    case op of+        T_IoDuplicate _ (T_GREATAND _) "-" -> True+        T_IoDuplicate _ (T_LESSAND  _) "-" -> True+        _                                  -> False++getEnableDirectives root =+    case root of+        T_Annotation _ list _ -> [s | EnableComment s <- list]+        _ -> []  return [] runTests = $quickCheckAll
src/ShellCheck/Analytics.hs view
@@ -1,5 +1,5 @@ {--    Copyright 2012-2021 Vidar Holen+    Copyright 2012-2022 Vidar Holen      This file is part of ShellCheck.     https://www.shellcheck.net@@ -19,13 +19,16 @@ -} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE FlexibleContexts #-}-module ShellCheck.Analytics (runAnalytics, optionalChecks, ShellCheck.Analytics.runTests) where+module ShellCheck.Analytics (checker, optionalChecks, ShellCheck.Analytics.runTests) where  import ShellCheck.AST import ShellCheck.ASTLib import ShellCheck.AnalyzerLib hiding (producesComments)+import ShellCheck.CFG+import qualified ShellCheck.CFGAnalysis as CF import ShellCheck.Data import ShellCheck.Parser+import ShellCheck.Prelude import ShellCheck.Interface import ShellCheck.Regex @@ -44,6 +47,7 @@ import Data.Semigroup import Debug.Trace -- STRIP import qualified Data.Map.Strict as Map+import qualified Data.Set as S import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) @@ -52,7 +56,6 @@ treeChecks = [     nodeChecksToTreeCheck nodeChecks     ,subshellAssignmentCheck-    ,checkSpacefulness     ,checkQuotesInLiterals     ,checkShebangParameters     ,checkFunctionsUsedExternally@@ -68,30 +71,23 @@     ,checkArrayValueUsedAsIndex     ] -runAnalytics :: AnalysisSpec -> [TokenComment]-runAnalytics options =-    runList options treeChecks ++ runList options optionalChecks+checker spec params = mkChecker spec params treeChecks++mkChecker spec params checks =+    Checker {+        perScript = \(Root root) -> do+            tell $ concatMap (\f -> f params root) all,+        perToken = const $ return ()+    }   where-    root = asScript options-    optionals = getEnableDirectives root ++ asOptionalChecks options-    optionalChecks =-        if "all" `elem` optionals+    all = checks ++ optionals+    optionalKeys = asOptionalChecks spec+    optionals =+        if "all" `elem` optionalKeys         then map snd optionalTreeChecks-        else mapMaybe (\c -> Map.lookup c optionalCheckMap) optionals+        else mapMaybe (\c -> Map.lookup c optionalCheckMap) optionalKeys -runList :: AnalysisSpec -> [Parameters -> Token -> [TokenComment]]-    -> [TokenComment]-runList spec list = notes-    where-        root = asScript spec-        params = makeParameters spec-        notes = concatMap (\f -> f params root) list -getEnableDirectives root =-    case root of-        T_Annotation _ list _ -> [s | EnableComment s <- list]-        _ -> []- checkList l t = concatMap (\f -> f t) l  -- Checks that are run on each node in the AST@@ -199,6 +195,12 @@     ,checkComparisonWithLeadingX     ,checkCommandWithTrailingSymbol     ,checkUnquotedParameterExpansionPattern+    ,checkBatsTestDoesNotUseNegation+    ,checkCommandIsUnreachable+    ,checkSpacefulnessCfg+    ,checkOverwrittenExitCode+    ,checkUnnecessaryArithmeticExpansionIndex+    ,checkUnnecessaryParens     ]  optionalChecks = map fst optionalTreeChecks@@ -217,7 +219,7 @@         cdDescription = "Suggest quoting variables without metacharacters",         cdPositive = "var=hello; echo $var",         cdNegative = "var=hello; echo \"$var\""-    }, checkVerboseSpacefulness)+    }, nodeChecksToTreeCheck [checkVerboseSpacefulnessCfg])      ,(newCheckDescription {         cdName = "avoid-nullary-conditions",@@ -309,12 +311,12 @@  runAndGetComments f s = do         let pr = pScript s-        prRoot pr+        root <- prRoot pr         let spec = defaultSpec pr         let params = makeParameters spec         return $             filterByAnnotation spec params $-                runList spec [f]+                f params root  -- Copied from https://wiki.haskell.org/Edit_distance dist :: Eq a => [a] -> [a] -> Int@@ -544,6 +546,11 @@ prop_checkPipePitfalls16 = verifyNot checkPipePitfalls "foo | grep -r bar | wc -l" prop_checkPipePitfalls17 = verifyNot checkPipePitfalls "foo | grep -l bar | wc -l" prop_checkPipePitfalls18 = verifyNot checkPipePitfalls "foo | grep -L bar | wc -l"+prop_checkPipePitfalls19 = verifyNot checkPipePitfalls "foo | grep -A2 bar | wc -l"+prop_checkPipePitfalls20 = verifyNot checkPipePitfalls "foo | grep -B999 bar | wc -l"+prop_checkPipePitfalls21 = verifyNot checkPipePitfalls "foo | grep --after-context 999 bar | wc -l"+prop_checkPipePitfalls22 = verifyNot checkPipePitfalls "foo | grep -B 1 --after-context 999 bar | wc -l"+prop_checkPipePitfalls23 = verifyNot checkPipePitfalls "ps -o pid,args -p $(pgrep java) | grep -F net.shellcheck.Test" checkPipePitfalls _ (T_Pipeline id _ commands) = do     for ["find", "xargs"] $         \(find:xargs:_) ->@@ -557,18 +564,25 @@               ]) $ warn (getId find) 2038                       "Use -print0/-0 or -exec + to allow for non-alphanumeric filenames." -    for' ["ps", "grep"] $-        \x -> info x 2009 "Consider using pgrep instead of grepping ps output."+    for ["ps", "grep"] $+        \(ps:grep:_) ->+            let+                psFlags = maybe [] (map snd . getAllFlags) $ getCommand ps+            in+                -- There are many ways to specify a pid: 1, -1, p 1, wup 1, -q 1, -p 1, --pid 1.+                -- For simplicity we only deal with the most canonical looking flags:+                unless (any (`elem` ["p", "pid", "q", "quick-pid"]) psFlags) $+                    info (getId ps) 2009 "Consider using pgrep instead of grepping ps output."      for ["grep", "wc"] $         \(grep:wc:_) ->             let flagsGrep = maybe [] (map snd . getAllFlags) $ getCommand grep                 flagsWc = maybe [] (map snd . getAllFlags) $ getCommand wc             in-                unless (any (`elem` ["l", "files-with-matches", "L", "files-without-matches", "o", "only-matching", "r", "R", "recursive"]) flagsGrep+                unless (any (`elem` ["l", "files-with-matches", "L", "files-without-matches", "o", "only-matching", "r", "R", "recursive", "A", "after-context", "B", "before-context"]) flagsGrep                         || any (`elem` ["m", "chars", "w", "words", "c", "bytes", "L", "max-line-length"]) flagsWc                         || null flagsWc) $-                    style (getId grep) 2126 "Consider using grep -c instead of grep|wc -l."+                    style (getId grep) 2126 "Consider using 'grep -c' instead of 'grep|wc -l'."      didLs <- fmap or . sequence $ [         for' ["ls", "grep"] $@@ -628,15 +642,15 @@ prop_checkShebang7 = verifyNotTree checkShebang "#!/usr/bin/env ash\n# shellcheck shell=sh\n" prop_checkShebang8 = verifyTree checkShebang "#!bin/sh\ntrue" prop_checkShebang9 = verifyNotTree checkShebang "# shellcheck shell=sh\ntrue"-prop_checkShebang10= verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"-prop_checkShebang11= verifyTree checkShebang "#!/bin/sh/\ntrue"-prop_checkShebang12= verifyTree checkShebang "#!/bin/sh/ -xe\ntrue"-prop_checkShebang13= verifyTree checkShebang "#!/bin/busybox sh"-prop_checkShebang14= verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n"-prop_checkShebang15= verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n"-prop_checkShebang16= verifyTree checkShebang "#!/bin/busybox ash"-prop_checkShebang17= verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n"-prop_checkShebang18= verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n"+prop_checkShebang10 = verifyNotTree checkShebang "#!foo\n# shellcheck shell=sh ignore=SC2239\ntrue"+prop_checkShebang11 = verifyTree checkShebang "#!/bin/sh/\ntrue"+prop_checkShebang12 = verifyTree checkShebang "#!/bin/sh/ -xe\ntrue"+prop_checkShebang13 = verifyTree checkShebang "#!/bin/busybox sh"+prop_checkShebang14 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=sh\n"+prop_checkShebang15 = verifyNotTree checkShebang "#!/bin/busybox sh\n# shellcheck shell=dash\n"+prop_checkShebang16 = verifyTree checkShebang "#!/bin/busybox ash"+prop_checkShebang17 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=dash\n"+prop_checkShebang18 = verifyNotTree checkShebang "#!/bin/busybox ash\n# shellcheck shell=sh\n" checkShebang params (T_Annotation _ list t) =     if any isOverride list then [] else checkShebang params t   where@@ -690,9 +704,9 @@ checkForInQuoted _ _ = return ()  prop_checkForInCat1 = verify checkForInCat "for f in $(cat foo); do stuff; done"-prop_checkForInCat1a= verify checkForInCat "for f in `cat foo`; do stuff; done"+prop_checkForInCat1a = verify checkForInCat "for f in `cat foo`; do stuff; done" prop_checkForInCat2 = verify checkForInCat "for f in $(cat foo | grep lol); do stuff; done"-prop_checkForInCat2a= verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done"+prop_checkForInCat2a = verify checkForInCat "for f in `cat foo | grep lol`; do stuff; done" prop_checkForInCat3 = verifyNot checkForInCat "for f in $(cat foo | grep bar | wc -l); do stuff; done" checkForInCat _ (T_ForIn _ f [T_NormalWord _ w] _) = mapM_ checkF w   where@@ -765,10 +779,10 @@   prop_checkUnquotedExpansions1 = verify checkUnquotedExpansions "rm $(ls)"-prop_checkUnquotedExpansions1a= verify checkUnquotedExpansions "rm `ls`"+prop_checkUnquotedExpansions1a = verify checkUnquotedExpansions "rm `ls`" prop_checkUnquotedExpansions2 = verify checkUnquotedExpansions "rm foo$(date)" prop_checkUnquotedExpansions3 = verify checkUnquotedExpansions "[ $(foo) == cow ]"-prop_checkUnquotedExpansions3a= verify checkUnquotedExpansions "[ ! $(foo) ]"+prop_checkUnquotedExpansions3a = verify checkUnquotedExpansions "[ ! $(foo) ]" prop_checkUnquotedExpansions4 = verifyNot checkUnquotedExpansions "[[ $(foo) == cow ]]" prop_checkUnquotedExpansions5 = verifyNot checkUnquotedExpansions "for f in $(cmd); do echo $f; done" prop_checkUnquotedExpansions6 = verifyNot checkUnquotedExpansions "$(cmd)"@@ -776,6 +790,7 @@ prop_checkUnquotedExpansions8 = verifyNot checkUnquotedExpansions "set -- $(seq 1 4)" prop_checkUnquotedExpansions9 = verifyNot checkUnquotedExpansions "echo foo `# inline comment`" prop_checkUnquotedExpansions10 = verify checkUnquotedExpansions "#!/bin/sh\nexport var=$(val)"+prop_checkUnquotedExpansions11 = verifyNot checkUnquotedExpansions "ps -p $(pgrep foo)" checkUnquotedExpansions params =     check   where@@ -789,7 +804,7 @@             warn (getId t) 2046 "Quote this to prevent word splitting."      shouldBeSplit t =-        getCommandNameFromExpansion t == Just "seq"+        getCommandNameFromExpansion t `elem` [Just "seq", Just "pgrep"]   prop_checkRedirectToSame = verify checkRedirectToSame "cat foo > foo"@@ -801,6 +816,7 @@ prop_checkRedirectToSame7 = verifyNot checkRedirectToSame "sed 's/foo/bar/g' file | sponge file" prop_checkRedirectToSame8 = verifyNot checkRedirectToSame "while read -r line; do _=\"$fname\"; done <\"$fname\"" prop_checkRedirectToSame9 = verifyNot checkRedirectToSame "while read -r line; do cat < \"$fname\"; done <\"$fname\""+prop_checkRedirectToSame10 = verifyNot checkRedirectToSame "mapfile -t foo <foo" checkRedirectToSame params s@(T_Pipeline _ _ list) =     mapM_ (\l -> (mapM_ (\x -> doAnalysis (checkOccurrences x) l) (getAllRedirs list))) list   where@@ -846,7 +862,7 @@     isHarmlessCommand arg = fromMaybe False $ do         cmd <- getClosestCommand (parentMap params) arg         name <- getCommandBasename cmd-        return $ name `elem` ["echo", "printf", "sponge"]+        return $ name `elem` ["echo", "mapfile", "printf", "sponge"]     containsAssignment arg = fromMaybe False $ do         cmd <- getClosestCommand (parentMap params) arg         return $ isAssignment cmd@@ -862,7 +878,7 @@ prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi" prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done" prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi"-checkShorthandIf params x@(T_AndIf id _ (T_OrIf _ _ (T_Pipeline _ _ t)))+checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ _) (T_Pipeline _ _ t))         | not (isOk t || inCondition) =     info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true."   where@@ -896,7 +912,7 @@   prop_checkUnquotedDollarAt = verify checkUnquotedDollarAt "ls $@"-prop_checkUnquotedDollarAt1= verifyNot checkUnquotedDollarAt "ls ${#@}"+prop_checkUnquotedDollarAt1 = verifyNot checkUnquotedDollarAt "ls ${#@}" prop_checkUnquotedDollarAt2 = verify checkUnquotedDollarAt "ls ${foo[@]}" prop_checkUnquotedDollarAt3 = verifyNot checkUnquotedDollarAt "ls ${#foo[@]}" prop_checkUnquotedDollarAt4 = verifyNot checkUnquotedDollarAt "ls \"$@\""@@ -1027,32 +1043,32 @@ prop_checkSingleQuotedVariables  = verify checkSingleQuotedVariables "echo '$foo'" prop_checkSingleQuotedVariables2 = verify checkSingleQuotedVariables "echo 'lol$1.jpg'" prop_checkSingleQuotedVariables3 = verifyNot checkSingleQuotedVariables "sed 's/foo$/bar/'"-prop_checkSingleQuotedVariables3a= verify checkSingleQuotedVariables "sed 's/${foo}/bar/'"-prop_checkSingleQuotedVariables3b= verify checkSingleQuotedVariables "sed 's/$(echo cow)/bar/'"-prop_checkSingleQuotedVariables3c= verify checkSingleQuotedVariables "sed 's/$((1+foo))/bar/'"+prop_checkSingleQuotedVariables3a = verify checkSingleQuotedVariables "sed 's/${foo}/bar/'"+prop_checkSingleQuotedVariables3b = verify checkSingleQuotedVariables "sed 's/$(echo cow)/bar/'"+prop_checkSingleQuotedVariables3c = verify checkSingleQuotedVariables "sed 's/$((1+foo))/bar/'" prop_checkSingleQuotedVariables4 = verifyNot checkSingleQuotedVariables "awk '{print $1}'" prop_checkSingleQuotedVariables5 = verifyNot checkSingleQuotedVariables "trap 'echo $SECONDS' EXIT" prop_checkSingleQuotedVariables6 = verifyNot checkSingleQuotedVariables "sed -n '$p'"-prop_checkSingleQuotedVariables6a= verify checkSingleQuotedVariables "sed -n '$pattern'"+prop_checkSingleQuotedVariables6a = verify checkSingleQuotedVariables "sed -n '$pattern'" prop_checkSingleQuotedVariables7 = verifyNot checkSingleQuotedVariables "PS1='$PWD \\$ '" prop_checkSingleQuotedVariables8 = verify checkSingleQuotedVariables "find . -exec echo '$1' {} +" prop_checkSingleQuotedVariables9 = verifyNot checkSingleQuotedVariables "find . -exec awk '{print $1}' {} \\;"-prop_checkSingleQuotedVariables10= verify checkSingleQuotedVariables "echo '`pwd`'"-prop_checkSingleQuotedVariables11= verifyNot checkSingleQuotedVariables "sed '${/lol/d}'"-prop_checkSingleQuotedVariables12= verifyNot checkSingleQuotedVariables "eval 'echo $1'"-prop_checkSingleQuotedVariables13= verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"-prop_checkSingleQuotedVariables14= verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"-prop_checkSingleQuotedVariables15= verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'"-prop_checkSingleQuotedVariables16= verify checkSingleQuotedVariables "git '$a'"-prop_checkSingleQuotedVariables17= verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"-prop_checkSingleQuotedVariables18= verifyNot checkSingleQuotedVariables "echo '``'"-prop_checkSingleQuotedVariables19= verifyNot checkSingleQuotedVariables "echo '```'"-prop_checkSingleQuotedVariables20= verifyNot checkSingleQuotedVariables "mumps -run %XCMD 'W $O(^GLOBAL(5))'"-prop_checkSingleQuotedVariables21= verifyNot checkSingleQuotedVariables "mumps -run LOOP%XCMD --xec 'W $O(^GLOBAL(6))'"-prop_checkSingleQuotedVariables22= verifyNot checkSingleQuotedVariables "jq '$__loc__'"-prop_checkSingleQuotedVariables23= verifyNot checkSingleQuotedVariables "command jq '$__loc__'"-prop_checkSingleQuotedVariables24= verifyNot checkSingleQuotedVariables "exec jq '$__loc__'"-prop_checkSingleQuotedVariables25= verifyNot checkSingleQuotedVariables "exec -c -a foo jq '$__loc__'"+prop_checkSingleQuotedVariables10 = verify checkSingleQuotedVariables "echo '`pwd`'"+prop_checkSingleQuotedVariables11 = verifyNot checkSingleQuotedVariables "sed '${/lol/d}'"+prop_checkSingleQuotedVariables12 = verifyNot checkSingleQuotedVariables "eval 'echo $1'"+prop_checkSingleQuotedVariables13 = verifyNot checkSingleQuotedVariables "busybox awk '{print $1}'"+prop_checkSingleQuotedVariables14 = verifyNot checkSingleQuotedVariables "[ -v 'bar[$foo]' ]"+prop_checkSingleQuotedVariables15 = verifyNot checkSingleQuotedVariables "git filter-branch 'test $GIT_COMMIT'"+prop_checkSingleQuotedVariables16 = verify checkSingleQuotedVariables "git '$a'"+prop_checkSingleQuotedVariables17 = verifyNot checkSingleQuotedVariables "rename 's/(.)a/$1/g' *"+prop_checkSingleQuotedVariables18 = verifyNot checkSingleQuotedVariables "echo '``'"+prop_checkSingleQuotedVariables19 = verifyNot checkSingleQuotedVariables "echo '```'"+prop_checkSingleQuotedVariables20 = verifyNot checkSingleQuotedVariables "mumps -run %XCMD 'W $O(^GLOBAL(5))'"+prop_checkSingleQuotedVariables21 = verifyNot checkSingleQuotedVariables "mumps -run LOOP%XCMD --xec 'W $O(^GLOBAL(6))'"+prop_checkSingleQuotedVariables22 = verifyNot checkSingleQuotedVariables "jq '$__loc__'"+prop_checkSingleQuotedVariables23 = verifyNot checkSingleQuotedVariables "command jq '$__loc__'"+prop_checkSingleQuotedVariables24 = verifyNot checkSingleQuotedVariables "exec jq '$__loc__'"+prop_checkSingleQuotedVariables25 = verifyNot checkSingleQuotedVariables "exec -c -a foo jq '$__loc__'"   checkSingleQuotedVariables params t@(T_SingleQuoted id s) =@@ -1161,6 +1177,10 @@ prop_checkNumberComparisons19 = verifyNot checkNumberComparisons "foo=1; [[ foo -eq 2 ]]" prop_checkNumberComparisons20 = verify checkNumberComparisons "[[ 2 -eq / ]]" prop_checkNumberComparisons21 = verify checkNumberComparisons "[[ foo -eq foo ]]"+prop_checkNumberComparisons22 = verify checkNumberComparisons "x=10; [[ $x > $z ]]"+prop_checkNumberComparisons23 = verify checkNumberComparisons "x=0; if [[ -n $def ]]; then x=$def; fi; while [ $x > $z ]; do lol; done"+prop_checkNumberComparisons24 = verify checkNumberComparisons "x=$RANDOM; [ $x > $z ]"+prop_checkNumberComparisons25 = verify checkNumberComparisons "[[ $((n++)) > $x ]]"  checkNumberComparisons params (TC_Binary id typ op lhs rhs) = do     if isNum lhs || isNum rhs@@ -1236,9 +1256,20 @@       numChar x = isDigit x || x `elem` "+-. "        isNum t =-        case oversimplify t of-            [v] -> all isDigit v-            _ -> False+        case getWordParts t of+            [T_DollarArithmetic {}] -> True+            [b@(T_DollarBraced id _ c)] ->+                let+                    str = concat $ oversimplify c+                    var = getBracedReference str+                in fromMaybe False $ do+                    state <- CF.getIncomingState (cfgAnalysis params) id+                    value <- Map.lookup var $ CF.variablesInScope state+                    return $ CF.numericalStatus (CF.variableValue value) >= CF.NumericalStatusMaybe+            _ ->+                case oversimplify t of+                    [v] -> all isDigit v+                    _ -> False        isFraction t =         case oversimplify t of@@ -1343,8 +1374,8 @@   prop_checkConstantIfs1 = verify checkConstantIfs "[[ foo != bar ]]"-prop_checkConstantIfs2a= verify checkConstantIfs "[ n -le 4 ]"-prop_checkConstantIfs2b= verifyNot checkConstantIfs "[[ n -le 4 ]]"+prop_checkConstantIfs2a = verify checkConstantIfs "[ n -le 4 ]"+prop_checkConstantIfs2b = verifyNot checkConstantIfs "[[ n -le 4 ]]" prop_checkConstantIfs3 = verify checkConstantIfs "[[ $n -le 4 && n != 2 ]]" prop_checkConstantIfs4 = verifyNot checkConstantIfs "[[ $n -le 3 ]]" prop_checkConstantIfs5 = verifyNot checkConstantIfs "[[ $n -le $n ]]"@@ -1449,13 +1480,14 @@ prop_checkArithmeticDeref7 = verifyNot checkArithmeticDeref "(( 10#$n ))" prop_checkArithmeticDeref8 = verifyNot checkArithmeticDeref "let i=$i+1" prop_checkArithmeticDeref9 = verifyNot checkArithmeticDeref "(( a[foo] ))"-prop_checkArithmeticDeref10= verifyNot checkArithmeticDeref "(( a[\\$foo] ))"-prop_checkArithmeticDeref11= verifyNot checkArithmeticDeref "a[$foo]=wee"-prop_checkArithmeticDeref12= verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done"-prop_checkArithmeticDeref13= verifyNot checkArithmeticDeref "(( $$ ))"-prop_checkArithmeticDeref14= verifyNot checkArithmeticDeref "(( $! ))"-prop_checkArithmeticDeref15= verifyNot checkArithmeticDeref "(( ${!var} ))"-prop_checkArithmeticDeref16= verifyNot checkArithmeticDeref "(( ${x+1} + ${x=42} ))"+prop_checkArithmeticDeref10 = verifyNot checkArithmeticDeref "(( a[\\$foo] ))"+prop_checkArithmeticDeref11 = verify checkArithmeticDeref "a[$foo]=wee"+prop_checkArithmeticDeref11b = verifyNot checkArithmeticDeref "declare -A a; a[$foo]=wee"+prop_checkArithmeticDeref12 = verify checkArithmeticDeref "for ((i=0; $i < 3; i)); do true; done"+prop_checkArithmeticDeref13 = verifyNot checkArithmeticDeref "(( $$ ))"+prop_checkArithmeticDeref14 = verifyNot checkArithmeticDeref "(( $! ))"+prop_checkArithmeticDeref15 = verifyNot checkArithmeticDeref "(( ${!var} ))"+prop_checkArithmeticDeref16 = verifyNot checkArithmeticDeref "(( ${x+1} + ${x=42} ))" checkArithmeticDeref params t@(TA_Expansion _ [T_DollarBraced id _ l]) =     unless (isException $ concat $ oversimplify l) getWarning   where@@ -1467,6 +1499,7 @@             T_Arithmetic {} -> return normalWarning             T_DollarArithmetic {} -> return normalWarning             T_ForArithmetic {} -> return normalWarning+            T_Assignment {} -> return normalWarning             T_SimpleCommand {} -> return noWarning             _ -> Nothing @@ -1590,7 +1623,7 @@  prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]" prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]"-prop_checkValidCondOps2a= verifyNot checkValidCondOps "[ 3 \\> 2 ]"+prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]" prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]" prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]" checkValidCondOps _ (TC_Binary id _ s _ _)@@ -1658,11 +1691,11 @@ checkTestRedirects _ _ = return ()  prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"-prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"+prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"-prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"+prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '" prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"@@ -1852,6 +1885,7 @@ prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.log 2>&1; bar" prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done" prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r"+prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :" checkSpuriousExec _ = doLists   where     doLists (T_Script _ _ cmds) = doList cmds False@@ -1867,7 +1901,7 @@      stripCleanup = reverse . dropWhile cleanup . reverse     cleanup (T_Pipeline _ _ [cmd]) =-        isCommandMatch cmd (`elem` ["echo", "exit", "printf", "return"])+        isCommandMatch cmd (`elem` [":", "echo", "exit", "printf", "return"])         || isAssignment cmd     cleanup _ = False @@ -1931,7 +1965,7 @@ prop_subshellAssignmentCheck4 = verifyNotTree subshellAssignmentCheck "( A=foo; rm $A; )" prop_subshellAssignmentCheck5 = verifyTree    subshellAssignmentCheck "cat foo | while read cow; do true; done; echo $cow;" prop_subshellAssignmentCheck6 = verifyTree    subshellAssignmentCheck "( export lol=$(ls); ); echo $lol;"-prop_subshellAssignmentCheck6a= verifyTree    subshellAssignmentCheck "( typeset -a lol=a; ); echo $lol;"+prop_subshellAssignmentCheck6a = verifyTree    subshellAssignmentCheck "( typeset -a lol=a; ); echo $lol;" prop_subshellAssignmentCheck7 = verifyTree    subshellAssignmentCheck "cmd | while read foo; do (( n++ )); done; echo \"$n lines\"" prop_subshellAssignmentCheck8 = verifyTree    subshellAssignmentCheck "n=3 & echo $((n++))" prop_subshellAssignmentCheck9 = verifyTree    subshellAssignmentCheck "read n & n=foo$n"@@ -2001,107 +2035,6 @@         writeFunc base token name values     doFlow _ = return [] ----- Check whether variables could have spaces/globs-prop_checkSpacefulness1 = verifyTree checkSpacefulness "a='cow moo'; echo $a"-prop_checkSpacefulness2 = verifyNotTree checkSpacefulness "a='cow moo'; [[ $a ]]"-prop_checkSpacefulness3 = verifyNotTree checkSpacefulness "a='cow*.mp3'; echo \"$a\""-prop_checkSpacefulness4 = verifyTree checkSpacefulness "for f in *.mp3; do echo $f; done"-prop_checkSpacefulness4a= verifyNotTree checkSpacefulness "foo=3; foo=$(echo $foo)"-prop_checkSpacefulness5 = verifyTree checkSpacefulness "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"-prop_checkSpacefulness6 = verifyTree checkSpacefulness "a=foo$(lol); echo $a"-prop_checkSpacefulness7 = verifyTree checkSpacefulness "a=foo\\ bar; rm $a"-prop_checkSpacefulness8 = verifyNotTree checkSpacefulness "a=foo\\ bar; a=foo; rm $a"-prop_checkSpacefulness10= verifyTree checkSpacefulness "rm $1"-prop_checkSpacefulness11= verifyTree checkSpacefulness "rm ${10//foo/bar}"-prop_checkSpacefulness12= verifyNotTree checkSpacefulness "(( $1 + 3 ))"-prop_checkSpacefulness13= verifyNotTree checkSpacefulness "if [[ $2 -gt 14 ]]; then true; fi"-prop_checkSpacefulness14= verifyNotTree checkSpacefulness "foo=$3 env"-prop_checkSpacefulness15= verifyNotTree checkSpacefulness "local foo=$1"-prop_checkSpacefulness16= verifyNotTree checkSpacefulness "declare foo=$1"-prop_checkSpacefulness17= verifyTree checkSpacefulness "echo foo=$1"-prop_checkSpacefulness18= verifyNotTree checkSpacefulness "$1 --flags"-prop_checkSpacefulness19= verifyTree checkSpacefulness "echo $PWD"-prop_checkSpacefulness20= verifyNotTree checkSpacefulness "n+='foo bar'"-prop_checkSpacefulness21= verifyNotTree checkSpacefulness "select foo in $bar; do true; done"-prop_checkSpacefulness22= verifyNotTree checkSpacefulness "echo $\"$1\""-prop_checkSpacefulness23= verifyNotTree checkSpacefulness "a=(1); echo ${a[@]}"-prop_checkSpacefulness24= verifyTree checkSpacefulness "a='a    b'; cat <<< $a"-prop_checkSpacefulness25= verifyTree checkSpacefulness "a='s/[0-9]//g'; sed $a"-prop_checkSpacefulness26= verifyTree checkSpacefulness "a='foo bar'; echo {1,2,$a}"-prop_checkSpacefulness27= verifyNotTree checkSpacefulness "echo ${a:+'foo'}"-prop_checkSpacefulness28= verifyNotTree checkSpacefulness "exec {n}>&1; echo $n"-prop_checkSpacefulness29= verifyNotTree checkSpacefulness "n=$(stuff); exec {n}>&-;"-prop_checkSpacefulness30= verifyTree checkSpacefulness "file='foo bar'; echo foo > $file;"-prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\\"`\""-prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"-prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"-prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"-prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"-prop_checkSpacefulness36= verifyNotTree checkSpacefulness "arg=$#; echo $arg"-prop_checkSpacefulness37= verifyNotTree checkSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"-prop_checkSpacefulness37v = verifyTree checkVerboseSpacefulness "@test 'status' {\n [ $status -eq 0 ]\n}"-prop_checkSpacefulness38= verifyTree checkSpacefulness "a=; echo $a"-prop_checkSpacefulness39= verifyNotTree checkSpacefulness "a=''\"\"''; b=x$a; echo $b"-prop_checkSpacefulness40= verifyNotTree checkSpacefulness "a=$((x+1)); echo $a"-prop_checkSpacefulness41= verifyNotTree checkSpacefulness "exec $1 --flags"-prop_checkSpacefulness42= verifyNotTree checkSpacefulness "run $1 --flags"-prop_checkSpacefulness43= verifyNotTree checkSpacefulness "$foo=42"-prop_checkSpacefulness44= verifyTree checkSpacefulness "#!/bin/sh\nexport var=$value"-prop_checkSpacefulness45= verifyNotTree checkSpacefulness "wait -zzx -p foo; echo $foo"-prop_checkSpacefulness46= verifyNotTree checkSpacefulness "x=0; (( x += 1 )); echo $x"--data SpaceStatus = SpaceSome | SpaceNone | SpaceEmpty deriving (Eq)-instance Semigroup SpaceStatus where-    SpaceNone <> SpaceNone = SpaceNone-    SpaceSome <> _ = SpaceSome-    _ <> SpaceSome = SpaceSome-    SpaceEmpty <> x = x-    x <> SpaceEmpty = x-instance Monoid SpaceStatus where-    mempty = SpaceEmpty-    mappend = (<>)---- This is slightly awkward because we want to support structured--- optional checks based on nearly the same logic-checkSpacefulness params = checkSpacefulness' onFind params-  where-    emit x = tell [x]-    onFind spaces token _ =-        when (spaces /= SpaceNone) $-            if isDefaultAssignment (parentMap params) token-            then-                emit $ makeComment InfoC (getId token) 2223-                         "This default assignment may cause DoS due to globbing. Quote it."-            else-                unless (quotesMayConflictWithSC2281 params token) $-                    emit $ makeCommentWithFix InfoC (getId token) 2086-                             "Double quote to prevent globbing and word splitting."-                                (addDoubleQuotesAround params token)--    isDefaultAssignment parents token =-        let modifier = getBracedModifier $ bracedString token in-            any (`isPrefixOf` modifier) ["=", ":="]-            && isParamTo parents ":" token--    -- Given a T_DollarBraced, return a simplified version of the string contents.-    bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l-    bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"--prop_checkSpacefulness4v= verifyTree checkVerboseSpacefulness "foo=3; foo=$(echo $foo)"-prop_checkSpacefulness8v= verifyTree checkVerboseSpacefulness "a=foo\\ bar; a=foo; rm $a"-prop_checkSpacefulness28v = verifyTree checkVerboseSpacefulness "exec {n}>&1; echo $n"-prop_checkSpacefulness36v = verifyTree checkVerboseSpacefulness "arg=$#; echo $arg"-prop_checkSpacefulness44v = verifyNotTree checkVerboseSpacefulness "foo=3; $foo=4"-checkVerboseSpacefulness params = checkSpacefulness' onFind params-  where-    onFind spaces token name =-        when (spaces == SpaceNone-                && name `notElem` specialVariablesWithoutSpaces-                && not (quotesMayConflictWithSC2281 params token)) $-            tell [makeCommentWithFix StyleC (getId token) 2248-                    "Prefer double quoting even when variables don't contain special characters."-                    (addDoubleQuotesAround params token)]- -- Don't suggest quotes if this will instead be autocorrected -- from $foo=bar to foo=bar. This is not pretty but ok. quotesMayConflictWithSC2281 params t =@@ -2111,76 +2044,126 @@         _ -> False  addDoubleQuotesAround params token = (surroundWith (getId token) params "\"")-checkSpacefulness'-    :: (SpaceStatus -> Token -> String -> Writer [TokenComment] ()) ->-            Parameters -> Token -> [TokenComment]-checkSpacefulness' onFind params t =-    doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)-  where-    defaults = zip variablesWithoutSpaces (repeat SpaceNone) -    hasSpaces name = gets (Map.findWithDefault SpaceSome name)+prop_checkSpacefulnessCfg1 = verify checkSpacefulnessCfg "a='cow moo'; echo $a"+prop_checkSpacefulnessCfg2 = verifyNot checkSpacefulnessCfg "a='cow moo'; [[ $a ]]"+prop_checkSpacefulnessCfg3 = verifyNot checkSpacefulnessCfg "a='cow*.mp3'; echo \"$a\""+prop_checkSpacefulnessCfg4 = verify checkSpacefulnessCfg "for f in *.mp3; do echo $f; done"+prop_checkSpacefulnessCfg4a = verifyNot checkSpacefulnessCfg "foo=3; foo=$(echo $foo)"+prop_checkSpacefulnessCfg5 = verify checkSpacefulnessCfg "a='*'; b=$a; c=lol${b//foo/bar}; echo $c"+prop_checkSpacefulnessCfg6 = verify checkSpacefulnessCfg "a=foo$(lol); echo $a"+prop_checkSpacefulnessCfg7 = verify checkSpacefulnessCfg "a=foo\\ bar; rm $a"+prop_checkSpacefulnessCfg8 = verifyNot checkSpacefulnessCfg "a=foo\\ bar; a=foo; rm $a"+prop_checkSpacefulnessCfg10 = verify checkSpacefulnessCfg "rm $1"+prop_checkSpacefulnessCfg11 = verify checkSpacefulnessCfg "rm ${10//foo/bar}"+prop_checkSpacefulnessCfg12 = verifyNot checkSpacefulnessCfg "(( $1 + 3 ))"+prop_checkSpacefulnessCfg13 = verifyNot checkSpacefulnessCfg "if [[ $2 -gt 14 ]]; then true; fi"+prop_checkSpacefulnessCfg14 = verifyNot checkSpacefulnessCfg "foo=$3 env"+prop_checkSpacefulnessCfg15 = verifyNot checkSpacefulnessCfg "local foo=$1"+prop_checkSpacefulnessCfg16 = verifyNot checkSpacefulnessCfg "declare foo=$1"+prop_checkSpacefulnessCfg17 = verify checkSpacefulnessCfg "echo foo=$1"+prop_checkSpacefulnessCfg18 = verifyNot checkSpacefulnessCfg "$1 --flags"+prop_checkSpacefulnessCfg19 = verify checkSpacefulnessCfg "echo $PWD"+prop_checkSpacefulnessCfg20 = verifyNot checkSpacefulnessCfg "n+='foo bar'"+prop_checkSpacefulnessCfg21 = verifyNot checkSpacefulnessCfg "select foo in $bar; do true; done"+prop_checkSpacefulnessCfg22 = verifyNot checkSpacefulnessCfg "echo $\"$1\""+prop_checkSpacefulnessCfg23 = verifyNot checkSpacefulnessCfg "a=(1); echo ${a[@]}"+prop_checkSpacefulnessCfg24 = verify checkSpacefulnessCfg "a='a    b'; cat <<< $a"+prop_checkSpacefulnessCfg25 = verify checkSpacefulnessCfg "a='s/[0-9]//g'; sed $a"+prop_checkSpacefulnessCfg26 = verify checkSpacefulnessCfg "a='foo bar'; echo {1,2,$a}"+prop_checkSpacefulnessCfg27 = verifyNot checkSpacefulnessCfg "echo ${a:+'foo'}"+prop_checkSpacefulnessCfg28 = verifyNot checkSpacefulnessCfg "exec {n}>&1; echo $n"+prop_checkSpacefulnessCfg29 = verifyNot checkSpacefulnessCfg "n=$(stuff); exec {n}>&-;"+prop_checkSpacefulnessCfg30 = verify checkSpacefulnessCfg "file='foo bar'; echo foo > $file;"+prop_checkSpacefulnessCfg31 = verifyNot checkSpacefulnessCfg "echo \"`echo \\\"$1\\\"`\""+prop_checkSpacefulnessCfg32 = verifyNot checkSpacefulnessCfg "var=$1; [ -v var ]"+prop_checkSpacefulnessCfg33 = verify checkSpacefulnessCfg "for file; do echo $file; done"+prop_checkSpacefulnessCfg34 = verify checkSpacefulnessCfg "declare foo$n=$1"+prop_checkSpacefulnessCfg35 = verifyNot checkSpacefulnessCfg "echo ${1+\"$1\"}"+prop_checkSpacefulnessCfg36 = verifyNot checkSpacefulnessCfg "arg=$#; echo $arg"+prop_checkSpacefulnessCfg37 = verifyNot checkSpacefulnessCfg "@test 'status' {\n [ $status -eq 0 ]\n}"+prop_checkSpacefulnessCfg37v = verify checkVerboseSpacefulnessCfg "@test 'status' {\n [ $status -eq 0 ]\n}"+prop_checkSpacefulnessCfg38 = verify checkSpacefulnessCfg "a=; echo $a"+prop_checkSpacefulnessCfg39 = verifyNot checkSpacefulnessCfg "a=''\"\"''; b=x$a; echo $b"+prop_checkSpacefulnessCfg40 = verifyNot checkSpacefulnessCfg "a=$((x+1)); echo $a"+prop_checkSpacefulnessCfg41 = verifyNot checkSpacefulnessCfg "exec $1 --flags"+prop_checkSpacefulnessCfg42 = verifyNot checkSpacefulnessCfg "run $1 --flags"+prop_checkSpacefulnessCfg43 = verifyNot checkSpacefulnessCfg "$foo=42"+prop_checkSpacefulnessCfg44 = verify checkSpacefulnessCfg "#!/bin/sh\nexport var=$value"+prop_checkSpacefulnessCfg45 = verifyNot checkSpacefulnessCfg "wait -zzx -p foo; echo $foo"+prop_checkSpacefulnessCfg46 = verifyNot checkSpacefulnessCfg "x=0; (( x += 1 )); echo $x"+prop_checkSpacefulnessCfg47 = verifyNot checkSpacefulnessCfg "x=0; (( x-- )); echo $x"+prop_checkSpacefulnessCfg48 = verifyNot checkSpacefulnessCfg "x=0; (( ++x )); echo $x"+prop_checkSpacefulnessCfg49 = verifyNot checkSpacefulnessCfg "for i in 1 2 3; do echo $i; done"+prop_checkSpacefulnessCfg50 = verify checkSpacefulnessCfg "for i in 1 2 *; do echo $i; done"+prop_checkSpacefulnessCfg51 = verify checkSpacefulnessCfg "x='foo bar'; x && x=1; echo $x"+prop_checkSpacefulnessCfg52 = verifyNot checkSpacefulnessCfg "x=1; if f; then x='foo bar'; exit; fi; echo $x"+prop_checkSpacefulnessCfg53 = verifyNot checkSpacefulnessCfg "s=1; f() { local s='a b'; }; f; echo $s"+prop_checkSpacefulnessCfg54 = verifyNot checkSpacefulnessCfg "s='a b'; f() { s=1; }; f; echo $s"+prop_checkSpacefulnessCfg55 = verify checkSpacefulnessCfg "s='a b'; x && f() { s=1; }; f; echo $s"+prop_checkSpacefulnessCfg56 = verifyNot checkSpacefulnessCfg "s=1; cat <(s='a b'); echo $s"+prop_checkSpacefulnessCfg57 = verifyNot checkSpacefulnessCfg "declare -i s=0; s=$(f); echo $s"+prop_checkSpacefulnessCfg58 = verify checkSpacefulnessCfg "f() { declare -i s; }; f; s=$(var); echo $s"+prop_checkSpacefulnessCfg59 = verifyNot checkSpacefulnessCfg "f() { declare -gi s; }; f; s=$(var); echo $s"+prop_checkSpacefulnessCfg60 = verify checkSpacefulnessCfg "declare -i s; declare +i s; s=$(foo); echo $s"+prop_checkSpacefulnessCfg61 = verify checkSpacefulnessCfg "declare -x X; y=foo$X; echo $y;"+prop_checkSpacefulnessCfg62 = verifyNot checkSpacefulnessCfg "f() { declare -x X; y=foo$X; echo $y; }"+prop_checkSpacefulnessCfg63 = verify checkSpacefulnessCfg "f && declare -i s; s='x + y'; echo $s"+prop_checkSpacefulnessCfg64 = verifyNot checkSpacefulnessCfg "declare -i s; s='x + y'; x=$s; echo $x"+prop_checkSpacefulnessCfg65 = verifyNot checkSpacefulnessCfg "f() { s=$?; echo $s; }; f"+prop_checkSpacefulnessCfg66 = verifyNot checkSpacefulnessCfg "f() { s=$?; echo $s; }" -    setSpaces name status =-        modify $ Map.insert name status+checkSpacefulnessCfg = checkSpacefulnessCfg' True+checkVerboseSpacefulnessCfg = checkSpacefulnessCfg' False -    readF _ token name = do-        spaces <- hasSpaces name-        let needsQuoting =-                  isExpansion token-                  && not (isArrayExpansion token) -- There's another warning for this-                  && not (isCountingReference token)-                  && not (isQuoteFree (shellType params) parents token)-                  && not (isQuotedAlternativeReference token)-                  && not (usedAsCommandName parents token)+checkSpacefulnessCfg' :: Bool -> (Parameters -> Token -> Writer [TokenComment] ())+checkSpacefulnessCfg' dirtyPass params token@(T_DollarBraced id _ list) =+    when (needsQuoting && (dirtyPass == not isClean)) $+        unless (name `elem` specialVariablesWithoutSpaces || quotesMayConflictWithSC2281 params token) $+            if dirtyPass+            then+                if isDefaultAssignment (parentMap params) token+                then+                    info (getId token) 2223+                             "This default assignment may cause DoS due to globbing. Quote it."+                else+                    infoWithFix id 2086 "Double quote to prevent globbing and word splitting." $+                        addDoubleQuotesAround params token+            else+                styleWithFix id 2248 "Prefer double quoting even when variables don't contain special characters." $+                    addDoubleQuotesAround params token -        return . execWriter $ when needsQuoting $ onFind spaces token name+  where+    name = getBracedReference $ concat $ oversimplify list+    parents = parentMap params+    needsQuoting =+              not (isArrayExpansion token) -- There's another warning for this+              && not (isCountingReference token)+              && not (isQuoteFree (shellType params) parents token)+              && not (isQuotedAlternativeReference token)+              && not (usedAsCommandName parents token) -      where-        emit x = tell [x]+    isClean = fromMaybe False $ do+        state <- CF.getIncomingState (cfgAnalysis params) id+        value <- Map.lookup name $ CF.variablesInScope state+        return $ isCleanState value -    writeF _ (TA_Assignment {}) name _ = setSpaces name SpaceNone >> return []-    writeF _ _ name (DataString SourceExternal) = setSpaces name SpaceSome >> return []-    writeF _ _ name (DataString SourceInteger) = setSpaces name SpaceNone >> return []+    isCleanState state =+        (all (S.member CFVPInteger) $ CF.variableProperties state)+        || CF.spaceStatus (CF.variableValue state) == CF.SpaceStatusClean -    writeF _ _ name (DataString (SourceFrom vals)) = do-        map <- get-        setSpaces name-            (isSpacefulWord (\x -> Map.findWithDefault SpaceSome x map) vals)-        return []+    isDefaultAssignment parents token =+        let modifier = getBracedModifier $ bracedString token in+            any (`isPrefixOf` modifier) ["=", ":="]+            && isParamTo parents ":" token -    writeF _ _ _ _ = return []+    -- Given a T_DollarBraced, return a simplified version of the string contents.+    bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l+    bracedString _ = error $ pleaseReport "bracedString on non-variable" -    parents = parentMap params+checkSpacefulnessCfg' _ _ _ = return () -    isExpansion t =-        case t of-            (T_DollarBraced _ _ _ ) -> True-            _ -> False -    isSpacefulWord :: (String -> SpaceStatus) -> [Token] -> SpaceStatus-    isSpacefulWord f = mconcat . map (isSpaceful f)-    isSpaceful :: (String -> SpaceStatus) -> Token -> SpaceStatus-    isSpaceful spacefulF x =-        case x of-          T_DollarExpansion _ _ -> SpaceSome-          T_Backticked _ _ -> SpaceSome-          T_Glob _ _         -> SpaceSome-          T_Extglob {}       -> SpaceSome-          T_DollarArithmetic _ _ -> SpaceNone-          T_Literal _ s      -> fromLiteral s-          T_SingleQuoted _ s -> fromLiteral s-          T_DollarBraced _ _ l -> spacefulF $ getBracedReference $ concat $ oversimplify l-          T_NormalWord _ w   -> isSpacefulWord spacefulF w-          T_DoubleQuoted _ w -> isSpacefulWord spacefulF w-          _ -> SpaceEmpty-      where-        globspace = "*?[] \t\n"-        containsAny s = any (`elem` s)-        fromLiteral "" = SpaceEmpty-        fromLiteral s | s `containsAny` globspace = SpaceSome-        fromLiteral _ = SpaceNone- prop_CheckVariableBraces1 = verify checkVariableBraces "a='123'; echo $a" prop_CheckVariableBraces2 = verifyNot checkVariableBraces "a='123'; echo ${a}" prop_CheckVariableBraces3 = verifyNot checkVariableBraces "#shellcheck disable=SC2016\necho '$a'"@@ -2198,13 +2181,13 @@ checkVariableBraces _ _ = return ()  prop_checkQuotesInLiterals1 = verifyTree checkQuotesInLiterals "param='--foo=\"bar\"'; app $param"-prop_checkQuotesInLiterals1a= verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param"+prop_checkQuotesInLiterals1a = verifyTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; app $param" prop_checkQuotesInLiterals2 = verifyNotTree checkQuotesInLiterals "param='--foo=\"bar\"'; app \"$param\"" prop_checkQuotesInLiterals3 =verifyNotTree checkQuotesInLiterals "param=('--foo='); app \"${param[@]}\"" prop_checkQuotesInLiterals4 = verifyNotTree checkQuotesInLiterals "param=\"don't bother with this one\"; app $param" prop_checkQuotesInLiterals5 = verifyNotTree checkQuotesInLiterals "param=\"--foo='lolbar'\"; eval app $param" prop_checkQuotesInLiterals6 = verifyTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm $param\"; $cmd"-prop_checkQuotesInLiterals6a= verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd"+prop_checkQuotesInLiterals6a = verifyNotTree checkQuotesInLiterals "param='my\\ file'; cmd=\"rm ${#param}\"; $cmd" prop_checkQuotesInLiterals7 = verifyTree checkQuotesInLiterals "param='my\\ file'; rm $param" prop_checkQuotesInLiterals8 = verifyTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm $param" prop_checkQuotesInLiterals9 = verifyNotTree checkQuotesInLiterals "param=\"/foo/'bar baz'/etc\"; rm ${#param}"@@ -2268,9 +2251,9 @@   verifyTree checkFunctionsUsedExternally "foo() { :; }; sudo foo" prop_checkFunctionsUsedExternally2 =   verifyTree checkFunctionsUsedExternally "alias f='a'; xargs -0 f"-prop_checkFunctionsUsedExternally2b=+prop_checkFunctionsUsedExternally2b =   verifyNotTree checkFunctionsUsedExternally "alias f='a'; find . -type f"-prop_checkFunctionsUsedExternally2c=+prop_checkFunctionsUsedExternally2c =   verifyTree checkFunctionsUsedExternally "alias f='a'; find . -type f -exec f +" prop_checkFunctionsUsedExternally3 =   verifyNotTree checkFunctionsUsedExternally "f() { :; }; echo f"@@ -2296,7 +2279,7 @@                 let args = skipOver t argv                 let argStrings = map (\x -> (fromMaybe "" $ getLiteralString x, x)) args                 let candidates = getPotentialCommands name argStrings-                mapM_ (checkArg name) candidates+                mapM_ (checkArg name (getId t)) candidates             _ -> return ()     checkCommand _ _ = return () @@ -2322,14 +2305,19 @@      functionsAndAliases = Map.union (functions t) (aliases t) -    checkArg cmd (_, arg) = sequence_ $ do+    patternContext id =+        case posLine . fst <$> Map.lookup id (tokenPositions params) of+          Just l -> " on line " <> show l <> "."+          _      -> "."++    checkArg cmd cmdId (_, arg) = sequence_ $ do         literalArg <- getUnquotedLiteral arg  -- only consider unquoted literals         definitionId <- Map.lookup literalArg functionsAndAliases         return $ do             warn (getId arg) 2033-              "Shell functions can't be passed to external commands."+              "Shell functions can't be passed to external commands. Use separate script or sh -c."             info definitionId 2032 $-              "Use own script or sh -c '..' to run this from " ++ cmd ++ "."+              "This function can't be invoked via " ++ cmd ++ patternContext cmdId  prop_checkUnused0 = verifyNotTree checkUnusedAssignments "var=foo; echo $var" prop_checkUnused1 = verifyTree checkUnusedAssignments "var=foo; echo $bar"@@ -2341,48 +2329,49 @@ prop_checkUnused7 = verifyNotTree checkUnusedAssignments "var=2; $((var))" prop_checkUnused8 = verifyTree checkUnusedAssignments "var=2; var=3;" prop_checkUnused9 = verifyNotTree checkUnusedAssignments "read ''"-prop_checkUnused10= verifyNotTree checkUnusedAssignments "read -p 'test: '"-prop_checkUnused11= verifyNotTree checkUnusedAssignments "bar=5; export foo[$bar]=3"-prop_checkUnused12= verifyNotTree checkUnusedAssignments "read foo; echo ${!foo}"-prop_checkUnused13= verifyNotTree checkUnusedAssignments "x=(1); (( x[0] ))"-prop_checkUnused14= verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}"-prop_checkUnused15= verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))"-prop_checkUnused16= verifyNotTree checkUnusedAssignments "foo=5; declare -x foo"-prop_checkUnused16b= verifyNotTree checkUnusedAssignments "f() { local -x foo; foo=42; bar; }; f"-prop_checkUnused17= verifyNotTree checkUnusedAssignments "read -i 'foo' -e -p 'Input: ' bar; $bar;"-prop_checkUnused18= verifyNotTree checkUnusedAssignments "a=1; arr=( [$a]=42 ); echo \"${arr[@]}\""-prop_checkUnused19= verifyNotTree checkUnusedAssignments "a=1; let b=a+1; echo $b"-prop_checkUnused20= verifyNotTree checkUnusedAssignments "a=1; PS1='$a'"-prop_checkUnused21= verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT"-prop_checkUnused22= verifyNotTree checkUnusedAssignments "a=1; [ -v a ]"-prop_checkUnused23= verifyNotTree checkUnusedAssignments "a=1; [ -R a ]"-prop_checkUnused24= verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}"-prop_checkUnused25= verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}"-prop_checkUnused26= verifyNotTree checkUnusedAssignments "declare -F foo"-prop_checkUnused27= verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"-prop_checkUnused28= verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"-prop_checkUnused29= verifyNotTree checkUnusedAssignments "var=(a b); declare -p var"-prop_checkUnused30= verifyTree checkUnusedAssignments "let a=1"-prop_checkUnused31= verifyTree checkUnusedAssignments "let 'a=1'"-prop_checkUnused32= verifyTree checkUnusedAssignments "let a=b=c; echo $a"-prop_checkUnused33= verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]"-prop_checkUnused34= verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t"-prop_checkUnused35= verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"-prop_checkUnused36= verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"-prop_checkUnused37= verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"-prop_checkUnused38= verifyTree checkUnusedAssignments "(( a=42 ))"-prop_checkUnused39= verifyNotTree checkUnusedAssignments "declare -x -f foo"-prop_checkUnused40= verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\""-prop_checkUnused41= verifyNotTree checkUnusedAssignments "@test 'foo' {\ntrue\n}\n"-prop_checkUnused42= verifyNotTree checkUnusedAssignments "DEFINE_string foo '' ''; echo \"${FLAGS_foo}\""-prop_checkUnused43= verifyTree checkUnusedAssignments "DEFINE_string foo '' ''"-prop_checkUnused44= verifyNotTree checkUnusedAssignments "DEFINE_string \"foo$ibar\" x y"-prop_checkUnused45= verifyTree checkUnusedAssignments "readonly foo=bar"-prop_checkUnused46= verifyTree checkUnusedAssignments "readonly foo=(bar)"-prop_checkUnused47= verifyNotTree checkUnusedAssignments "a=1; alias hello='echo $a'"-prop_checkUnused48= verifyNotTree checkUnusedAssignments "_a=1"-prop_checkUnused49= verifyNotTree checkUnusedAssignments "declare -A array; key=a; [[ -v array[$key] ]]"-prop_checkUnused50= verifyNotTree checkUnusedAssignments "foofunc() { :; }; typeset -fx foofunc"+prop_checkUnused10 = verifyNotTree checkUnusedAssignments "read -p 'test: '"+prop_checkUnused11 = verifyNotTree checkUnusedAssignments "bar=5; export foo[$bar]=3"+prop_checkUnused12 = verifyNotTree checkUnusedAssignments "read foo; echo ${!foo}"+prop_checkUnused13 = verifyNotTree checkUnusedAssignments "x=(1); (( x[0] ))"+prop_checkUnused14 = verifyNotTree checkUnusedAssignments "x=(1); n=0; echo ${x[n]}"+prop_checkUnused15 = verifyNotTree checkUnusedAssignments "x=(1); n=0; (( x[n] ))"+prop_checkUnused16 = verifyNotTree checkUnusedAssignments "foo=5; declare -x foo"+prop_checkUnused16b = verifyNotTree checkUnusedAssignments "f() { local -x foo; foo=42; bar; }; f"+prop_checkUnused17 = verifyNotTree checkUnusedAssignments "read -i 'foo' -e -p 'Input: ' bar; $bar;"+prop_checkUnused18 = verifyNotTree checkUnusedAssignments "a=1; arr=( [$a]=42 ); echo \"${arr[@]}\""+prop_checkUnused19 = verifyNotTree checkUnusedAssignments "a=1; let b=a+1; echo $b"+prop_checkUnused20 = verifyNotTree checkUnusedAssignments "a=1; PS1='$a'"+prop_checkUnused21 = verifyNotTree checkUnusedAssignments "a=1; trap 'echo $a' INT"+prop_checkUnused22 = verifyNotTree checkUnusedAssignments "a=1; [ -v a ]"+prop_checkUnused23 = verifyNotTree checkUnusedAssignments "a=1; [ -R a ]"+prop_checkUnused24 = verifyNotTree checkUnusedAssignments "mapfile -C a b; echo ${b[@]}"+prop_checkUnused25 = verifyNotTree checkUnusedAssignments "readarray foo; echo ${foo[@]}"+prop_checkUnused26 = verifyNotTree checkUnusedAssignments "declare -F foo"+prop_checkUnused27 = verifyTree checkUnusedAssignments "var=3; [ var -eq 3 ]"+prop_checkUnused28 = verifyNotTree checkUnusedAssignments "var=3; [[ var -eq 3 ]]"+prop_checkUnused29 = verifyNotTree checkUnusedAssignments "var=(a b); declare -p var"+prop_checkUnused30 = verifyTree checkUnusedAssignments "let a=1"+prop_checkUnused31 = verifyTree checkUnusedAssignments "let 'a=1'"+prop_checkUnused32 = verifyTree checkUnusedAssignments "let a=b=c; echo $a"+prop_checkUnused33 = verifyNotTree checkUnusedAssignments "a=foo; [[ foo =~ ^{$a}$ ]]"+prop_checkUnused34 = verifyNotTree checkUnusedAssignments "foo=1; (( t = foo )); echo $t"+prop_checkUnused35 = verifyNotTree checkUnusedAssignments "a=foo; b=2; echo ${a:b}"+prop_checkUnused36 = verifyNotTree checkUnusedAssignments "if [[ -v foo ]]; then true; fi"+prop_checkUnused37 = verifyNotTree checkUnusedAssignments "fd=2; exec {fd}>&-"+prop_checkUnused38 = verifyTree checkUnusedAssignments "(( a=42 ))"+prop_checkUnused39 = verifyNotTree checkUnusedAssignments "declare -x -f foo"+prop_checkUnused40 = verifyNotTree checkUnusedAssignments "arr=(1 2); num=2; echo \"${arr[@]:num}\""+prop_checkUnused41 = verifyNotTree checkUnusedAssignments "@test 'foo' {\ntrue\n}\n"+prop_checkUnused42 = verifyNotTree checkUnusedAssignments "DEFINE_string foo '' ''; echo \"${FLAGS_foo}\""+prop_checkUnused43 = verifyTree checkUnusedAssignments "DEFINE_string foo '' ''"+prop_checkUnused44 = verifyNotTree checkUnusedAssignments "DEFINE_string \"foo$ibar\" x y"+prop_checkUnused45 = verifyTree checkUnusedAssignments "readonly foo=bar"+prop_checkUnused46 = verifyTree checkUnusedAssignments "readonly foo=(bar)"+prop_checkUnused47 = verifyNotTree checkUnusedAssignments "a=1; alias hello='echo $a'"+prop_checkUnused48 = verifyNotTree checkUnusedAssignments "_a=1"+prop_checkUnused49 = verifyNotTree checkUnusedAssignments "declare -A array; key=a; [[ -v array[$key] ]]"+prop_checkUnused50 = verifyNotTree checkUnusedAssignments "foofunc() { :; }; typeset -fx foofunc"+prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@]}"  checkUnusedAssignments params t = execWriter (mapM_ warnFor unused)   where@@ -2416,40 +2405,40 @@ prop_checkUnassignedReferences7 = verifyNotTree checkUnassignedReferences "getopts ':h' foo; echo $foo" prop_checkUnassignedReferences8 = verifyNotTree checkUnassignedReferences "let 'foo = 1'; echo $foo" prop_checkUnassignedReferences9 = verifyNotTree checkUnassignedReferences "echo ${foo-bar}"-prop_checkUnassignedReferences10= verifyNotTree checkUnassignedReferences "echo ${foo:?}"-prop_checkUnassignedReferences11= verifyNotTree checkUnassignedReferences "declare -A foo; echo \"${foo[@]}\""-prop_checkUnassignedReferences12= verifyNotTree checkUnassignedReferences "typeset -a foo; echo \"${foo[@]}\""-prop_checkUnassignedReferences13= verifyNotTree checkUnassignedReferences "f() { local foo; echo $foo; }"-prop_checkUnassignedReferences14= verifyNotTree checkUnassignedReferences "foo=; echo $foo"-prop_checkUnassignedReferences15= verifyNotTree checkUnassignedReferences "f() { true; }; export -f f"-prop_checkUnassignedReferences16= verifyNotTree checkUnassignedReferences "declare -A foo=( [a b]=bar ); echo ${foo[a b]}"-prop_checkUnassignedReferences17= verifyNotTree checkUnassignedReferences "USERS=foo; echo $USER"-prop_checkUnassignedReferences18= verifyNotTree checkUnassignedReferences "FOOBAR=42; export FOOBAR="-prop_checkUnassignedReferences19= verifyNotTree checkUnassignedReferences "readonly foo=bar; echo $foo"-prop_checkUnassignedReferences20= verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo"-prop_checkUnassignedReferences21= verifyTree checkUnassignedReferences "echo ${#foo}"-prop_checkUnassignedReferences22= verifyNotTree checkUnassignedReferences "echo ${!os*}"-prop_checkUnassignedReferences23= verifyTree checkUnassignedReferences "declare -a foo; foo[bar]=42;"-prop_checkUnassignedReferences24= verifyNotTree checkUnassignedReferences "declare -A foo; foo[bar]=42;"-prop_checkUnassignedReferences25= verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;"-prop_checkUnassignedReferences26= verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"-prop_checkUnassignedReferences27= verifyNotTree checkUnassignedReferences ": ${foo:=bar}"-prop_checkUnassignedReferences28= verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n"-prop_checkUnassignedReferences29= verifyNotTree checkUnassignedReferences "if [[ -v foo ]]; then echo $foo; fi"-prop_checkUnassignedReferences30= verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"-prop_checkUnassignedReferences31= verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"-prop_checkUnassignedReferences32= verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"-prop_checkUnassignedReferences33= verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"-prop_checkUnassignedReferences34= verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"-prop_checkUnassignedReferences35= verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"-prop_checkUnassignedReferences36= verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""-prop_checkUnassignedReferences37= verifyNotTree checkUnassignedReferences "var=howdy; printf -v 'array[0]' %s \"$var\"; printf %s \"${array[0]}\";"-prop_checkUnassignedReferences38= verifyTree (checkUnassignedReferences' True) "echo $VAR"-prop_checkUnassignedReferences39= verifyNotTree checkUnassignedReferences "builtin export var=4; echo $var"-prop_checkUnassignedReferences40= verifyNotTree checkUnassignedReferences ": ${foo=bar}"-prop_checkUnassignedReferences41= verifyNotTree checkUnassignedReferences "mapfile -t files 123; echo \"${files[@]}\""-prop_checkUnassignedReferences42= verifyNotTree checkUnassignedReferences "mapfile files -t; echo \"${files[@]}\""-prop_checkUnassignedReferences43= verifyNotTree checkUnassignedReferences "mapfile --future files; echo \"${files[@]}\""+prop_checkUnassignedReferences10 = verifyNotTree checkUnassignedReferences "echo ${foo:?}"+prop_checkUnassignedReferences11 = verifyNotTree checkUnassignedReferences "declare -A foo; echo \"${foo[@]}\""+prop_checkUnassignedReferences12 = verifyNotTree checkUnassignedReferences "typeset -a foo; echo \"${foo[@]}\""+prop_checkUnassignedReferences13 = verifyNotTree checkUnassignedReferences "f() { local foo; echo $foo; }"+prop_checkUnassignedReferences14 = verifyNotTree checkUnassignedReferences "foo=; echo $foo"+prop_checkUnassignedReferences15 = verifyNotTree checkUnassignedReferences "f() { true; }; export -f f"+prop_checkUnassignedReferences16 = verifyNotTree checkUnassignedReferences "declare -A foo=( [a b]=bar ); echo ${foo[a b]}"+prop_checkUnassignedReferences17 = verifyNotTree checkUnassignedReferences "USERS=foo; echo $USER"+prop_checkUnassignedReferences18 = verifyNotTree checkUnassignedReferences "FOOBAR=42; export FOOBAR="+prop_checkUnassignedReferences19 = verifyNotTree checkUnassignedReferences "readonly foo=bar; echo $foo"+prop_checkUnassignedReferences20 = verifyNotTree checkUnassignedReferences "printf -v foo bar; echo $foo"+prop_checkUnassignedReferences21 = verifyTree checkUnassignedReferences "echo ${#foo}"+prop_checkUnassignedReferences22 = verifyNotTree checkUnassignedReferences "echo ${!os*}"+prop_checkUnassignedReferences23 = verifyTree checkUnassignedReferences "declare -a foo; foo[bar]=42;"+prop_checkUnassignedReferences24 = verifyNotTree checkUnassignedReferences "declare -A foo; foo[bar]=42;"+prop_checkUnassignedReferences25 = verifyNotTree checkUnassignedReferences "declare -A foo=(); foo[bar]=42;"+prop_checkUnassignedReferences26 = verifyNotTree checkUnassignedReferences "a::b() { foo; }; readonly -f a::b"+prop_checkUnassignedReferences27 = verifyNotTree checkUnassignedReferences ": ${foo:=bar}"+prop_checkUnassignedReferences28 = verifyNotTree checkUnassignedReferences "#!/bin/ksh\necho \"${.sh.version}\"\n"+prop_checkUnassignedReferences29 = verifyNotTree checkUnassignedReferences "if [[ -v foo ]]; then echo $foo; fi"+prop_checkUnassignedReferences30 = verifyNotTree checkUnassignedReferences "if [[ -v foo[3] ]]; then echo ${foo[3]}; fi"+prop_checkUnassignedReferences31 = verifyNotTree checkUnassignedReferences "X=1; if [[ -v foo[$X+42] ]]; then echo ${foo[$X+42]}; fi"+prop_checkUnassignedReferences32 = verifyNotTree checkUnassignedReferences "if [[ -v \"foo[1]\" ]]; then echo ${foo[@]}; fi"+prop_checkUnassignedReferences33 = verifyNotTree checkUnassignedReferences "f() { local -A foo; echo \"${foo[@]}\"; }"+prop_checkUnassignedReferences34 = verifyNotTree checkUnassignedReferences "declare -A foo; (( foo[bar] ))"+prop_checkUnassignedReferences35 = verifyNotTree checkUnassignedReferences "echo ${arr[foo-bar]:?fail}"+prop_checkUnassignedReferences36 = verifyNotTree checkUnassignedReferences "read -a foo -r <<<\"foo bar\"; echo \"$foo\""+prop_checkUnassignedReferences37 = verifyNotTree checkUnassignedReferences "var=howdy; printf -v 'array[0]' %s \"$var\"; printf %s \"${array[0]}\";"+prop_checkUnassignedReferences38 = verifyTree (checkUnassignedReferences' True) "echo $VAR"+prop_checkUnassignedReferences39 = verifyNotTree checkUnassignedReferences "builtin export var=4; echo $var"+prop_checkUnassignedReferences40 = verifyNotTree checkUnassignedReferences ": ${foo=bar}"+prop_checkUnassignedReferences41 = verifyNotTree checkUnassignedReferences "mapfile -t files 123; echo \"${files[@]}\""+prop_checkUnassignedReferences42 = verifyNotTree checkUnassignedReferences "mapfile files -t; echo \"${files[@]}\""+prop_checkUnassignedReferences43 = verifyNotTree checkUnassignedReferences "mapfile --future files; echo \"${files[@]}\"" prop_checkUnassignedReferences_minusNPlain   = verifyNotTree checkUnassignedReferences "if [ -n \"$x\" ]; then echo $x; fi" prop_checkUnassignedReferences_minusZPlain   = verifyNotTree checkUnassignedReferences "if [ -z \"$x\" ]; then echo \"\"; fi" prop_checkUnassignedReferences_minusNBraced  = verifyNotTree checkUnassignedReferences "if [ -n \"${x}\" ]; then echo $x; fi"@@ -2827,11 +2816,11 @@ prop_checkUnpassedInFunctions7 = verifyTree checkUnpassedInFunctions "foo() { echo $1; }; foo; foo;" prop_checkUnpassedInFunctions8 = verifyNotTree checkUnpassedInFunctions "foo() { echo $((1)); }; foo;" prop_checkUnpassedInFunctions9 = verifyNotTree checkUnpassedInFunctions "foo() { echo $(($b)); }; foo;"-prop_checkUnpassedInFunctions10= verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;"-prop_checkUnpassedInFunctions11= verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"-prop_checkUnpassedInFunctions12= verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;"-prop_checkUnpassedInFunctions13= verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n"-prop_checkUnpassedInFunctions14= verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo"+prop_checkUnpassedInFunctions10 = verifyNotTree checkUnpassedInFunctions "foo() { echo $!; }; foo;"+prop_checkUnpassedInFunctions11 = verifyNotTree checkUnpassedInFunctions "foo() { bar() { echo $1; }; bar baz; }; foo;"+prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;"+prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n"+prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo" checkUnpassedInFunctions params root =     execWriter $ mapM_ warnForGroup referenceGroups   where@@ -3006,12 +2995,12 @@ checkSuspiciousIFS _ _ = return ()  -prop_checkGrepQ1= verify checkShouldUseGrepQ "[[ $(foo | grep bar) ]]"-prop_checkGrepQ2= verify checkShouldUseGrepQ "[ -z $(fgrep lol) ]"-prop_checkGrepQ3= verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]"-prop_checkGrepQ4= verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]"-prop_checkGrepQ5= verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"-prop_checkGrepQ6= verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]"+prop_checkGrepQ1 = verify checkShouldUseGrepQ "[[ $(foo | grep bar) ]]"+prop_checkGrepQ2 = verify checkShouldUseGrepQ "[ -z $(fgrep lol) ]"+prop_checkGrepQ3 = verify checkShouldUseGrepQ "[ -n \"$(foo | zgrep lol)\" ]"+prop_checkGrepQ4 = verifyNot checkShouldUseGrepQ "[ -z $(grep bar | cmd) ]"+prop_checkGrepQ5 = verifyNot checkShouldUseGrepQ "rm $(ls | grep file)"+prop_checkGrepQ6 = verifyNot checkShouldUseGrepQ "[[ -n $(pgrep foo) ]]" checkShouldUseGrepQ params t =     sequence_ $ case t of         TC_Nullary id _ token -> check id True token@@ -3313,6 +3302,7 @@             _:next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next             _:next@(TC_Group {}):_ -> isOnlyTestInCommand next             _:next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next+            _:next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next             _ -> False      -- TODO: Do better $? tracking and filter on whether@@ -3595,7 +3585,6 @@ prop_checkPipeToNowhere5 = verifyNot checkPipeToNowhere "echo foo | xargs du" prop_checkPipeToNowhere6 = verifyNot checkPipeToNowhere "ls | echo $(cat)" prop_checkPipeToNowhere7 = verifyNot checkPipeToNowhere "echo foo | var=$(cat) ls"-prop_checkPipeToNowhere8 = verify checkPipeToNowhere "foo | true" prop_checkPipeToNowhere9 = verifyNot checkPipeToNowhere "mv -i f . < /dev/stdin" prop_checkPipeToNowhere10 = verify checkPipeToNowhere "ls > file | grep foo" prop_checkPipeToNowhere11 = verify checkPipeToNowhere "ls | grep foo < file"@@ -4115,13 +4104,6 @@     checkUnit :: [Token] -> Writer [TokenComment] ()     checkUnit unit = evalStateT (mapM_ (doAnalysis findCommands) unit) (Map.empty) -    isSourced t =-        let-            f (T_SourceCommand {}) = True-            f _ = False-        in-            any f $ getPath (parentMap params) t-     findCommands :: Token -> StateT (Map.Map String Token) (Writer [TokenComment]) ()     findCommands t = case t of             T_SimpleCommand _ _ (cmd:args) ->@@ -4132,7 +4114,7 @@                         cmd <- gets (Map.lookup name)                         case cmd of                             Just alias ->-                                unless (isSourced t || shouldIgnoreCode params 2262 alias) $ do+                                unless (isSourced params t || shouldIgnoreCode params 2262 alias) $ do                                     warn (getId alias) 2262 "This alias can't be defined and used in the same parsing unit. Use a function instead."                                     info (getId t) 2263 "Since they're in the same parsing unit, this command will not refer to the previously mentioned alias."                             _ -> return ()@@ -4143,6 +4125,14 @@         when (isVariableName name && not (null value)) $             modify (Map.insertWith (\new old -> old) name arg) +isSourced params t =+    let+        f (T_SourceCommand {}) = True+        f _ = False+    in+        any f $ getPath (parentMap params) t++ -- Like groupBy, but compares pairs of adjacent elements, rather than against the first of the span prop_groupByLink1 = groupByLink (\a b -> a+1 == b) [1,2,3,2,3,7,8,9] == [[1,2,3], [2,3], [7,8,9]] prop_groupByLink2 = groupByLink (==) ([] :: [()]) == []@@ -4706,6 +4696,7 @@ prop_checkSetESuppressed16 = verifyTree    checkSetESuppressed "set -e; f(){ :; }; until set -e; f; do :; done" prop_checkSetESuppressed17 = verifyNotTree checkSetESuppressed "set -e; f(){ :; }; g(){ :; }; g f" prop_checkSetESuppressed18 = verifyNotTree checkSetESuppressed "set -e; shopt -s inherit_errexit; f(){ :; }; x=$(f)"+prop_checkSetESuppressed19 = verifyNotTree checkSetESuppressed "set -e; set -o posix; f(){ :; }; x=$(f)" checkSetESuppressed params t =     if hasSetE params then runNodeAnalysis checkNode params t else []   where@@ -4783,8 +4774,12 @@ prop_checkExtraMaskedReturns33 = verifyNotTree checkExtraMaskedReturns "{ false || true; } | true" prop_checkExtraMaskedReturns34 = verifyNotTree checkExtraMaskedReturns "{ false || :; } | true" prop_checkExtraMaskedReturns35 = verifyTree checkExtraMaskedReturns "f() { local -r x=$(false); }"+prop_checkExtraMaskedReturns36 = verifyNotTree checkExtraMaskedReturns "time false"+prop_checkExtraMaskedReturns37 = verifyNotTree checkExtraMaskedReturns "time $(time false)"+prop_checkExtraMaskedReturns38 = verifyTree checkExtraMaskedReturns "x=$(time time time false) time $(time false)" -checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t+checkExtraMaskedReturns params t =+    runNodeAnalysis findMaskingNodes params (removeTransparentCommands t)   where     findMaskingNodes _ (T_Arithmetic _ list) = findMaskedNodesInList [list]     findMaskingNodes _ (T_Array _ list) = findMaskedNodesInList $ allButLastSimpleCommands list@@ -4817,6 +4812,13 @@       where         simpleCommands = filter containsSimpleCommand cmds +    removeTransparentCommands t =+        doTransform go t+      where+        go cmd@(T_SimpleCommand id assigns (_:args)) | isTransparentCommand cmd+          = T_SimpleCommand id assigns args+        go t = t+     inform t = info (getId t) 2312 ("Consider invoking this command "         ++ "separately to avoid masking its return value (or use '|| true' "         ++ "to ignore).")@@ -4849,12 +4851,163 @@             ,"shopt"             ] +    isTransparentCommand t = fromMaybe False $ do+        basename <- getCommandBasename t+        return $ basename == "time"+     parentChildPairs t = go $ parents params t       where         go (child:parent:rest) = (parent, child):go (parent:rest)         go _ = []      hasParent pred t = any (uncurry pred) (parentChildPairs t)+++-- hard error on negated command that is not last+prop_checkBatsTestDoesNotUseNegation1 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! true;  false; }"+prop_checkBatsTestDoesNotUseNegation2 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]]; false; }"+prop_checkBatsTestDoesNotUseNegation3 = verify checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ]; false; }"+-- acceptable formats:+--     using run+prop_checkBatsTestDoesNotUseNegation4 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { run ! true; }"+--     using || false+prop_checkBatsTestDoesNotUseNegation5 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]] || false; }"+prop_checkBatsTestDoesNotUseNegation6 = verifyNot checkBatsTestDoesNotUseNegation "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ] || false; }"+-- only style warning when last command+prop_checkBatsTestDoesNotUseNegation7 = verifyCodes checkBatsTestDoesNotUseNegation [2314] "#!/usr/bin/env/bats\n@test \"name\" { ! true; }"+prop_checkBatsTestDoesNotUseNegation8 = verifyCodes checkBatsTestDoesNotUseNegation [2315] "#!/usr/bin/env/bats\n@test \"name\" { ! [[ -e test ]]; }"+prop_checkBatsTestDoesNotUseNegation9 = verifyCodes checkBatsTestDoesNotUseNegation [2315] "#!/usr/bin/env/bats\n@test \"name\" { ! [ -e test ]; }"++checkBatsTestDoesNotUseNegation params t =+    case t of+        T_BatsTest _ _ (T_BraceGroup _ commands) -> mapM_ (check commands) commands+        _ -> return ()+  where+    check commands t =+        case t of+            T_Banged id (T_Pipeline _ _ [T_Redirecting _ _ (T_Condition idCondition _ _)]) ->+                                if t `isLastOf` commands+                                then style id 2315 "In Bats, ! will not fail the test if it is not the last command anymore. Fold the `!` into the conditional!"+                                else err   id 2315 "In Bats, ! does not cause a test failure. Fold the `!` into the conditional!"++            T_Banged id cmd -> if t `isLastOf` commands+                                then styleWithFix id 2314 "In Bats, ! will not fail the test if it is not the last command anymore. Use `run ! ` (on Bats >= 1.5.0) instead."+                                                (fixWith [replaceStart id params 0 "run "])+                                else errWithFix   id 2314 "In Bats, ! does not cause a test failure. Use 'run ! ' (on Bats >= 1.5.0) instead."+                                                (fixWith [replaceStart id params 0 "run "])+            _ -> return ()+    isLastOf t commands =+        case commands of+            [x] -> x == t+            x:rest -> isLastOf t rest+            [] -> False+++prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz"+prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz"+prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz"+checkCommandIsUnreachable params t =+    case t of+        T_Pipeline {} -> sequence_ $ do+            state <- CF.getIncomingState (cfgAnalysis params) id+            guard . not $ CF.stateIsReachable state+            guard . not $ isSourced params t+            return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)."+        _ -> return ()+  where id = getId t+++prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]"+prop_checkOverwrittenExitCode2 = verifyNot checkOverwrittenExitCode "x; [ $? -eq 1 ]"+prop_checkOverwrittenExitCode3 = verify checkOverwrittenExitCode "x; echo \"Exit is $?\"; [ $? -eq 0 ]"+prop_checkOverwrittenExitCode4 = verifyNot checkOverwrittenExitCode "x; [ $? -eq 0 ] && echo Success"+prop_checkOverwrittenExitCode5 = verify checkOverwrittenExitCode "x; if [ $? -eq 0 ]; then var=$?; fi"+prop_checkOverwrittenExitCode6 = verify checkOverwrittenExitCode "x; [ $? -gt 0 ] && fail=$?"+prop_checkOverwrittenExitCode7 = verifyNot checkOverwrittenExitCode "[ 1 -eq 2 ]; status=$?"+prop_checkOverwrittenExitCode8 = verifyNot checkOverwrittenExitCode "[ 1 -eq 2 ]; exit $?"+checkOverwrittenExitCode params t =+    case t of+        T_DollarBraced id _ val | getLiteralString val == Just "?" -> check id+        _ -> return ()+  where+    check id = sequence_ $ do+        state <- CF.getIncomingState (cfgAnalysis params) id+        let exitCodeIds = CF.exitCodes state+        guard . not $ S.null exitCodeIds++        let idToToken = idMap params+        exitCodeTokens <- sequence $ map (\k -> Map.lookup k idToToken) $ S.toList exitCodeIds+        return $ do+            when (all isCondition exitCodeTokens && not (usedUnconditionally t exitCodeIds)) $+                warn id 2319 "This $? refers to a condition, not a command. Assign to a variable to avoid it being overwritten."+            when (all isPrinting exitCodeTokens) $+                warn id 2320 "This $? refers to echo/printf, not a previous command. Assign to variable to avoid it being overwritten."++    isCondition t =+        case t of+            T_Condition {} -> True+            T_SimpleCommand {} -> getCommandName t == Just "test"+            _ -> False++    -- If we don't do anything based on the condition, assume we wanted the condition itself+    -- This helps differentiate `x; [ $? -gt 0 ] && exit $?` vs `[ cond ]; exit $?`+    usedUnconditionally t testIds =+        all (\c -> CF.doesPostDominate (cfgAnalysis params) (getId t) c) testIds++    isPrinting t =+        case getCommandBasename t of+            Just "echo" -> True+            Just "printf" -> True+            _ -> False+++prop_checkUnnecessaryArithmeticExpansionIndex1 = verify checkUnnecessaryArithmeticExpansionIndex "a[$((1+1))]=n"+prop_checkUnnecessaryArithmeticExpansionIndex2 = verifyNot checkUnnecessaryArithmeticExpansionIndex "a[1+1]=n"+prop_checkUnnecessaryArithmeticExpansionIndex3 = verifyNot checkUnnecessaryArithmeticExpansionIndex "a[$(echo $((1+1)))]=n"+prop_checkUnnecessaryArithmeticExpansionIndex4 = verifyNot checkUnnecessaryArithmeticExpansionIndex "declare -A a; a[$((1+1))]=val"+checkUnnecessaryArithmeticExpansionIndex params t =+    case t of+        T_Assignment _ mode var [TA_Sequence _ [ TA_Expansion _ [expansion@(T_DollarArithmetic id _)]]] val ->+            styleWithFix id 2321 "Array indices are already arithmetic contexts. Prefer removing the $(( and ))." $ fix id+        _ -> return ()++  where+    fix id =+        fixWith [+            replaceStart id params 3 "", -- Remove "$(("+            replaceEnd id params 2 ""    -- Remove "))"+        ]+++prop_checkUnnecessaryParens1 = verify checkUnnecessaryParens "echo $(( ((1+1)) ))"+prop_checkUnnecessaryParens2 = verify checkUnnecessaryParens "x[((1+1))+1]=1"+prop_checkUnnecessaryParens3 = verify checkUnnecessaryParens "x[(1+1)]=1"+prop_checkUnnecessaryParens4 = verify checkUnnecessaryParens "$(( (x) ))"+prop_checkUnnecessaryParens5 = verify checkUnnecessaryParens "(( (x) ))"+prop_checkUnnecessaryParens6 = verifyNot checkUnnecessaryParens "x[(1+1)+1]=1"+prop_checkUnnecessaryParens7 = verifyNot checkUnnecessaryParens "(( (1*1)+1 ))"+prop_checkUnnecessaryParens8 = verifyNot checkUnnecessaryParens "(( (1)+1 ))"+checkUnnecessaryParens params t =+    case t of+        T_DollarArithmetic _ t -> checkLeading "$(( (x) )) is the same as $(( x ))" t+        T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))")  [x,y,z]+        T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t+        T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t+        TA_Parentesis _ (TA_Sequence _ [ TA_Parentesis id _ ]) ->+            styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id+        _ -> return ()+  where++    checkLeading str t =+        case t of+            TA_Sequence _ [TA_Parentesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id+            _ -> return ()++    fix id =+        fixWith [+            replaceStart id params 1 "", -- Remove "("+            replaceEnd id params 1 ""    -- Remove ")"+        ]   return []
src/ShellCheck/Analyzer.hs view
@@ -1,5 +1,5 @@ {--    Copyright 2012-2019 Vidar Holen+    Copyright 2012-2022 Vidar Holen      This file is part of ShellCheck.     https://www.shellcheck.net@@ -25,6 +25,7 @@ import Data.List import Data.Monoid import qualified ShellCheck.Checks.Commands+import qualified ShellCheck.Checks.ControlFlow import qualified ShellCheck.Checks.Custom import qualified ShellCheck.Checks.ShellSupport @@ -34,19 +35,21 @@ analyzeScript spec = newAnalysisResult {     arComments =         filterByAnnotation spec params . nub $-            runAnalytics spec-            ++ runChecker params (checkers spec params)+            runChecker params (checkers spec params) }   where     params = makeParameters spec  checkers spec params = mconcat $ map ($ params) [+    ShellCheck.Analytics.checker spec,     ShellCheck.Checks.Commands.checker spec,+    ShellCheck.Checks.ControlFlow.checker spec,     ShellCheck.Checks.Custom.checker,     ShellCheck.Checks.ShellSupport.checker     ]  optionalChecks = mconcat $ [     ShellCheck.Analytics.optionalChecks,-    ShellCheck.Checks.Commands.optionalChecks+    ShellCheck.Checks.Commands.optionalChecks,+    ShellCheck.Checks.ControlFlow.optionalChecks     ]
src/ShellCheck/AnalyzerLib.hs view
@@ -1,5 +1,5 @@ {--    Copyright 2012-2021 Vidar Holen+    Copyright 2012-2022 Vidar Holen      This file is part of ShellCheck.     https://www.shellcheck.net@@ -23,13 +23,16 @@  import ShellCheck.AST import ShellCheck.ASTLib+import qualified ShellCheck.CFGAnalysis as CF import ShellCheck.Data import ShellCheck.Interface import ShellCheck.Parser+import ShellCheck.Prelude import ShellCheck.Regex  import Control.Arrow (first) import Control.DeepSeq+import Control.Monad import Control.Monad.Identity import Control.Monad.RWS import Control.Monad.State@@ -87,6 +90,8 @@     hasPipefail        :: Bool,     -- A linear (bad) analysis of data flow     variableFlow       :: [StackData],+    -- A map from Id to Token+    idMap              :: Map.Map Id Token,     -- A map from Id to parent Token     parentMap          :: Map.Map Id Token,     -- The shell type, such as Bash or Ksh@@ -96,7 +101,9 @@     -- The root node of the AST     rootNode           :: Token,     -- map from token id to start and end position-    tokenPositions     :: Map.Map Id (Position, Position)+    tokenPositions     :: Map.Map Id (Position, Position),+    -- Result from Control Flow Graph analysis (including data flow analysis)+    cfgAnalysis :: CF.CFGAnalysis     } deriving (Show)  -- TODO: Cache results of common AST ops here@@ -189,35 +196,42 @@         }     in force withFix -makeParameters spec =-    let params = Parameters {+makeParameters spec = params+  where+    params = Parameters {         rootNode = root,         shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,         hasSetE = containsSetE root,         hasLastpipe =             case shellType params of-                Bash -> containsLastpipe root+                Bash -> isOptionSet "lastpipe" root                 Dash -> False                 Sh   -> False                 Ksh  -> True,         hasInheritErrexit =             case shellType params of-                Bash -> containsInheritErrexit root+                Bash -> isOptionSet "inherit_errexit" root                 Dash -> True                 Sh   -> True                 Ksh  -> False,         hasPipefail =             case shellType params of-                Bash -> containsPipefail root+                Bash -> isOptionSet "pipefail" root                 Dash -> True                 Sh   -> True-                Ksh  -> containsPipefail root,+                Ksh  -> isOptionSet "pipefail" root,         shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),+        idMap = getTokenMap root,         parentMap = getParentTree root,         variableFlow = getVariableFlow params root,-        tokenPositions = asTokenPositions spec-    } in params-  where root = asScript spec+        tokenPositions = asTokenPositions spec,+        cfgAnalysis = CF.analyzeControlFlow cfParams root+    }+    cfParams = CF.CFGParameters {+        CF.cfLastpipe = hasLastpipe params,+        CF.cfPipefail = hasPipefail params+    }+    root = asScript spec   -- Does this script mention 'set -e' anywhere?@@ -234,13 +248,14 @@             _ -> False     re = mkRegex "[[:space:]]-[^-]*e" -containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root++containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root   where     isPipefail t =         case t of             T_SimpleCommand {}  ->                 t `isUnqualifiedCommand` "set" &&-                    ("pipefail" `elem` oversimplify t ||+                    (opt `elem` oversimplify t ||                         "o" `elem` map snd (getAllFlags t))             _ -> False @@ -254,12 +269,8 @@                         (shopt `elem` oversimplify t)                 _ -> False --- Does this script mention 'shopt -s inherit_errexit' anywhere?-containsInheritErrexit = containsShopt "inherit_errexit"---- Does this script mention 'shopt -s lastpipe' anywhere?--- Also used as a hack.-containsLastpipe = containsShopt "lastpipe"+-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?+isOptionSet opt root = containsShopt opt root || containsSetOption opt root   prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh@@ -351,7 +362,7 @@             T_SelectIn {}                   -> return (not strict)             _                               -> Nothing -    -- Check whether this assigment is self-quoting due to being a recognized+    -- Check whether this assignment is self-quoting due to being a recognized     -- assignment passed to a Declaration Utility. This will soon be required     -- by POSIX: https://austingroupbugs.net/view.php?id=351     assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)@@ -408,12 +419,6 @@         getId word == currentId || getId (getCommandTokenOrThis t) == currentId     go _ _ = False --- A list of the element and all its parents up to the root node.-getPath tree t = t :-    case Map.lookup (getId t) tree of-        Nothing     -> []-        Just parent -> getPath tree parent- -- Version of the above taking the map from the current context -- Todo: give this the name "getPath" getPathM t = do@@ -511,13 +516,11 @@         T_SimpleCommand {} ->             getModifiedVariableCommand t -        TA_Unary _ "++|" v@(TA_Variable _ name _)  ->-            [(t, v, name, DataString $ SourceFrom [v])]-        TA_Unary _ "|++" v@(TA_Variable _ name _)  ->-            [(t, v, name, DataString $ SourceFrom [v])]+        TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->+            [(t, v, name, DataString SourceInteger)]         TA_Assignment _ op (TA_Variable _ name _) rhs -> do             guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]-            return (t, t, name, DataString $ SourceFrom [rhs])+            return (t, t, name, DataString SourceInteger)          T_BatsTest {} -> [             (t, t, "lines", DataArray SourceExternal),@@ -561,13 +564,7 @@                     return (place, t, str, DataString SourceChecked)             _ -> Nothing -isClosingFileOp op =-    case op of-        T_IoDuplicate _ (T_GREATAND _) "-" -> True-        T_IoDuplicate _ (T_LESSAND  _) "-" -> True-        _                                  -> False - -- Consider 'export/declare -x' a reference, since it makes the var available getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =     case x of@@ -748,13 +745,6 @@  getModifiedVariableCommand _ = [] -getIndexReferences s = fromMaybe [] $ do-    match <- matchRegex re s-    index <- match !!! 0-    return $ matchAllStrings variableNameRegex index-  where-    re = mkRegex "(\\[.*\\])"- -- Given a NormalWord like foo or foo[$bar], get foo. -- Primarily used to get references for [[ -v foo[bar] ]] getVariableForTestDashV :: Token -> Maybe String@@ -769,18 +759,6 @@     -- in a non-constant expression (while filtering out foo$x[$y])     toStr _ = return "\0" -prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]-prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]-prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]-prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]-getOffsetReferences mods = fromMaybe [] $ do--- if mods start with [, then drop until ]-    match <- matchRegex re mods-    offsets <- match !!! 1-    return $ matchAllStrings variableNameRegex offsets-  where-    re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"- getReferencedVariables parents t =     case t of         T_DollarBraced id _ l -> let str = concat $ oversimplify l in@@ -859,17 +837,6 @@ isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True isConfusedGlobRegex _       = False -isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x-isVariableChar x = isVariableStartChar x || isDigit x-isSpecialVariableChar = (`elem` "*@#?-$!")-variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"--prop_isVariableName1 = isVariableName "_fo123"-prop_isVariableName2 = not $ isVariableName "4"-prop_isVariableName3 = not $ isVariableName "test: "-isVariableName (x:r) = isVariableStartChar x && all isVariableChar r-isVariableName _     = False- getVariablesFromLiteralToken token =     getVariablesFromLiteral (getLiteralStringDef " " token) @@ -882,74 +849,7 @@   where     variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" --- Get the variable name from an expansion like ${var:-foo}-prop_getBracedReference1 = getBracedReference "foo" == "foo"-prop_getBracedReference2 = getBracedReference "#foo" == "foo"-prop_getBracedReference3 = getBracedReference "#" == "#"-prop_getBracedReference4 = getBracedReference "##" == "#"-prop_getBracedReference5 = getBracedReference "#!" == "!"-prop_getBracedReference6 = getBracedReference "!#" == "#"-prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"-prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"-prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"-prop_getBracedReference10= getBracedReference "foo: -1" == "foo"-prop_getBracedReference11= getBracedReference "!os*" == ""-prop_getBracedReference11b= getBracedReference "!os@" == ""-prop_getBracedReference12= getBracedReference "!os?bar**" == ""-prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"-getBracedReference s = fromMaybe s $-    nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s-  where-    noPrefix = dropPrefix s-    dropPrefix (c:rest) | c `elem` "!#" = rest-    dropPrefix cs = cs-    takeName s = do-        let name = takeWhile isVariableChar s-        guard . not $ null name-        return name-    getSpecial (c:_) | isSpecialVariableChar c = return [c]-    getSpecial _ = fail "empty or not special" -    nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}-        guard $ isVariableChar next -- e.g. ${!@}-        first <- find (not . isVariableChar) rest-        guard $ first `elem` "*?@"-        return ""-    nameExpansion _ = Nothing--prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"-prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"-prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"-prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"-prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"-getBracedModifier s = headOrDefault "" $ do-    let var = getBracedReference s-    a <- dropModifier s-    dropPrefix var a-  where-    dropPrefix [] t        = return t-    dropPrefix (a:b) (c:d) | a == c = dropPrefix b d-    dropPrefix _ _         = []--    dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]-    dropModifier x        = [x]---- Useful generic functions.---- Get element 0 or a default. Like `head` but safe.-headOrDefault _ (a:_) = a-headOrDefault def _   = def---- Get the last element or a default. Like `last` but safe.-lastOrDefault def [] = def-lastOrDefault _ list = last list----- Get element n of a list, or Nothing. Like `!!` but safe.-(!!!) list i =-    case drop i list of-        []    -> Nothing-        (r:_) -> Just r- -- Run a command if the shell is in the given list whenShell l c = do     params <- ask@@ -1000,17 +900,6 @@         Ksh -> True         Dash -> False         Sh -> False---- Returns whether a token is a parameter expansion without any modifiers.--- True for $var ${var} $1 $#--- False for ${#var} ${var[x]} ${var:-0}-isUnmodifiedParameterExpansion t =-    case t of-        T_DollarBraced _ False _ -> True-        T_DollarBraced _ _ list ->-            let str = concat $ oversimplify list-            in getBracedReference str == str-        _ -> False  isTrueAssignmentSource c =     case c of
+ src/ShellCheck/CFG.hs view
@@ -0,0 +1,1307 @@+{-+    Copyright 2022 Vidar Holen++    This file is part of ShellCheck.+    https://www.shellcheck.net++    ShellCheck is free software: you can redistribute it and/or modify+    it under the terms of the GNU General Public License as published by+    the Free Software Foundation, either version 3 of the License, or+    (at your option) any later version.++    ShellCheck is distributed in the hope that it will be useful,+    but WITHOUT ANY WARRANTY; without even the implied warranty of+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+    GNU General Public License for more details.++    You should have received a copy of the GNU General Public License+    along with this program.  If not, see <https://www.gnu.org/licenses/>.+-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-}++-- Constructs a Control Flow Graph from an AST+module ShellCheck.CFG (+    CFNode (..),+    CFEdge (..),+    CFEffect (..),+    CFStringPart (..),+    CFVariableProp (..),+    CFGResult (..),+    CFValue (..),+    CFGraph,+    CFGParameters (..),+    IdTagged (..),+    Scope (..),+    buildGraph+    , ShellCheck.CFG.runTests -- STRIP+    )+  where++import GHC.Generics (Generic)+import ShellCheck.AST+import ShellCheck.ASTLib+import ShellCheck.Data+import ShellCheck.Interface+import ShellCheck.Prelude+import ShellCheck.Regex+import Control.DeepSeq+import Control.Monad+import Control.Monad.Identity+import Data.Array.Unboxed+import Data.Array.ST+import Data.List hiding (map)+import Data.Maybe+import qualified Data.Map as M+import qualified Data.Set as S+import Control.Monad.RWS.Lazy+import Data.Graph.Inductive.Graph+import Data.Graph.Inductive.Query.DFS+import Data.Graph.Inductive.Basic+import Data.Graph.Inductive.Query.Dominators+import Data.Graph.Inductive.PatriciaTree as G+import Debug.Trace -- STRIP++import Test.QuickCheck.All (forAllProperties)+import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)+++-- Our basic Graph type+type CFGraph = G.Gr CFNode CFEdge++-- Node labels in a Control Flow Graph+data CFNode =+    -- A no-op node for structural purposes+    CFStructuralNode+    -- A no-op for graph inspection purposes+    | CFEntryPoint String+    -- Drop current prefix assignments+    | CFDropPrefixAssignments+    -- A node with a certain effect on program state+    | CFApplyEffects [IdTagged CFEffect]+    -- The execution of a command or function by literal string if possible+    | CFExecuteCommand (Maybe String)+    -- Execute a subshell. These are represented by disjoint graphs just like+    -- functions, but they don't require any form of name resolution+    | CFExecuteSubshell String Node Node+    -- Assignment of $?+    | CFSetExitCode Id+    -- The virtual 'exit' at the natural end of a subshell+    | CFImpliedExit+    -- An exit statement resolvable at CFG build time+    | CFResolvedExit+    -- An exit statement only resolvable at DFA time+    | CFUnresolvedExit+    -- An unreachable node, serving as the unconnected end point of a range+    | CFUnreachable+    -- Assignment of $!+    | CFSetBackgroundPid Id+  deriving (Eq, Ord, Show, Generic, NFData)++-- Edge labels in a Control Flow Graph+data CFEdge =+    CFEErrExit+    -- Regular control flow edge+    | CFEFlow+    -- An edge that a human might think exists (e.g. from a backgrounded process to its parent)+    | CFEFalseFlow+    -- An edge followed on exit+    | CFEExit+  deriving (Eq, Ord, Show, Generic, NFData)++-- Actions we track+data CFEffect =+    CFSetProps Scope String (S.Set CFVariableProp)+    | CFUnsetProps Scope String (S.Set CFVariableProp)+    | CFReadVariable String+    | CFWriteVariable String CFValue+    | CFWriteGlobal String CFValue+    | CFWriteLocal String CFValue+    | CFWritePrefix String CFValue+    | CFDefineFunction String Id Node Node+    | CFUndefine String+    | CFUndefineVariable String+    | CFUndefineFunction String+    | CFUndefineNameref String+    -- Usage implies that this is an array (e.g. it's expanded with index)+    | CFHintArray String+    -- Operation implies that the variable will be defined (e.g. [ -z "$var" ])+    | CFHintDefined String+  deriving (Eq, Ord, Show, Generic, NFData)++data IdTagged a = IdTagged Id a+  deriving (Eq, Ord, Show, Generic, NFData)++-- Where a variable's value comes from+data CFValue =+    -- The special 'uninitialized' value+    CFValueUninitialized+    -- An arbitrary array value+    | CFValueArray+    -- An arbitrary string value+    | CFValueString+    -- An arbitrary integer+    | CFValueInteger+    -- Token 'Id' concatenates and assigns the given parts+    | CFValueComputed Id [CFStringPart]+  deriving (Eq, Ord, Show, Generic, NFData)++-- Simplified computed strings+data CFStringPart =+    -- A known literal string value, like 'foo'+    CFStringLiteral String+    -- The contents of a variable, like $foo (may not be a string)+    | CFStringVariable String+    -- An value that is unknown but an integer+    | CFStringInteger+    -- An unknown string value, for things we can't handle+    | CFStringUnknown+  deriving (Eq, Ord, Show, Generic, NFData)++-- The properties of a variable+data CFVariableProp = CFVPExport | CFVPArray | CFVPAssociative | CFVPInteger+  deriving (Eq, Ord, Show, Generic, NFData)++-- Options when generating CFG+data CFGParameters = CFGParameters {+    -- Whether the last element in a pipeline runs in the current shell+    cfLastpipe :: Bool,+    -- Whether all elements in a pipeline count towards the exit status+    cfPipefail :: Bool+}++data CFGResult = CFGResult {+    -- The graph itself+    cfGraph :: CFGraph,+    -- Map from Id to nominal start&end node (i.e. assuming normal execution without exits)+    cfIdToRange :: M.Map Id (Node, Node),+    -- A set of all nodes belonging to an Id, recursively+    cfIdToNodes :: M.Map Id (S.Set Node),+    -- An array (from,to) saying whether 'from' postdominates 'to'+    cfPostDominators :: Array Node [Node]+}+  deriving (Show)++buildGraph :: CFGParameters -> Token -> CFGResult+buildGraph params root =+    let+        (nextNode, base) = execRWS (buildRoot root) (newCFContext params) 0+        (nodes, edges, mapping, association) =+--            renumberTopologically $+                removeUnnecessaryStructuralNodes+                    base++        idToRange = M.fromList mapping+        isRealEdge (from, to, edge) = case edge of CFEFlow -> True; _ -> False+        onlyRealEdges = filter isRealEdge edges+        (_, mainExit) = fromJust $ M.lookup (getId root) idToRange++        result = CFGResult {+            cfGraph = mkGraph nodes edges,+            cfIdToRange = idToRange,+            cfIdToNodes = M.fromListWith S.union $ map (\(id, n) -> (id, S.singleton n)) association,+            cfPostDominators = findPostDominators mainExit $ mkGraph nodes onlyRealEdges+        }+    in+        result++remapGraph :: M.Map Node Node -> CFW -> CFW+remapGraph remap (nodes, edges, mapping, assoc) =+    (+        map (remapNode remap) nodes,+        map (remapEdge remap) edges,+        map (\(id, (a,b)) -> (id, (remapHelper remap a, remapHelper remap b))) mapping,+        map (\(id, n) -> (id, remapHelper remap n)) assoc+    )++prop_testRenumbering =+    let+        s = CFStructuralNode+        before = (+            [(1,s), (3,s), (4, s), (8,s)],+            [(1,3,CFEFlow), (3,4, CFEFlow), (4,8,CFEFlow)],+            [(Id 0, (3,4))],+            [(Id 1, 3), (Id 2, 4)]+            )+        after = (+            [(0,s), (1,s), (2,s), (3,s)],+            [(0,1,CFEFlow), (1,2, CFEFlow), (2,3,CFEFlow)],+            [(Id 0, (1,2))],+            [(Id 1, 1), (Id 2, 2)]+            )+    in after == renumberGraph before++-- Renumber the graph for prettiness, so there are no gaps in node numbers+renumberGraph :: CFW -> CFW+renumberGraph g@(nodes, edges, mapping, assoc) =+    let renumbering = M.fromList (flip zip [0..] $ sort $ map fst nodes)+    in remapGraph renumbering g++prop_testRenumberTopologically =+    let+        s = CFStructuralNode+        before = (+            [(4,s), (2,s), (3, s)],+            [(4,2,CFEFlow), (2,3, CFEFlow)],+            [(Id 0, (4,2))],+            []+            )+        after = (+            [(0,s), (1,s), (2,s)],+            [(0,1,CFEFlow), (1,2, CFEFlow)],+            [(Id 0, (0,1))],+            []+            )+    in after == renumberTopologically before++-- Renumber the graph in topological order+renumberTopologically g@(nodes, edges, mapping, assoc) =+    let renumbering = M.fromList (flip zip [0..] $ topsort (mkGraph nodes edges :: CFGraph))+    in remapGraph renumbering g++prop_testRemoveStructural =+    let+        s = CFStructuralNode+        before = (+            [(1,s), (2,s), (3, s), (4,s)],+            [(1,2,CFEFlow), (2,3, CFEFlow), (3,4,CFEFlow)],+            [(Id 0, (2,3))],+            [(Id 0, 3)]+            )+        after = (+            [(1,s), (2,s), (4,s)],+            [(1,2,CFEFlow), (2,4,CFEFlow)],+            [(Id 0, (2,2))],+            [(Id 0, 2)]+            )+    in after == removeUnnecessaryStructuralNodes before++-- Collapse structural nodes that just form long chains like x->x->x.+-- This way we can generate them with abandon, without making DFA slower.+--+-- Note in particular that we can't remove a structural node x in+-- foo -> x -> bar , because then the pre/post-condition for tokens+-- previously pointing to x would be wrong.+removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) =+    remapGraph recursiveRemapping+        (+            filter (\(n, _) -> n `M.notMember` recursiveRemapping) nodes,+            filter (`S.notMember` edgesToCollapse) edges,+            mapping,+            association+        )+  where+    regularEdges = filter isRegularEdge edges+    inDegree = counter $ map (\(from,to,_) -> from) regularEdges+    outDegree = counter $ map (\(from,to,_) -> to) regularEdges+    structuralNodes = S.fromList $ map fst $ filter isStructural nodes+    candidateNodes = S.filter isLinear structuralNodes+    edgesToCollapse = S.fromList $ filter filterEdges regularEdges++    remapping :: M.Map Node Node+    remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse+    recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping++    filterEdges (a,b,_) =+        a `S.member` candidateNodes && b `S.member` candidateNodes++    orderEdge (a,b,_) = if a < b then (a,b) else (b,a)+    counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty+    isRegularEdge (_, _, CFEFlow) = True+    isRegularEdge _ = False++    recursiveLookup :: M.Map Node Node -> Node -> Node+    recursiveLookup map node =+        case M.lookup node map of+            Nothing -> node+            Just x -> recursiveLookup map x++    isStructural (node, label) =+        case label of+            CFStructuralNode -> True+            _ -> False++    isLinear node =+        M.findWithDefault 0 node inDegree == 1+        && M.findWithDefault 0 node outDegree == 1+++remapNode :: M.Map Node Node -> LNode CFNode -> LNode CFNode+remapNode m (node, label) =+    (remapHelper m node, newLabel)+  where+    newLabel = case label of+        CFApplyEffects effects -> CFApplyEffects (map (remapEffect m) effects)+        CFExecuteSubshell s a b -> CFExecuteSubshell s (remapHelper m a) (remapHelper m b)+        _ -> label++remapEffect map old@(IdTagged id effect) =+    case effect of+        CFDefineFunction name id start end -> IdTagged id $ CFDefineFunction name id (remapHelper map start) (remapHelper map end)+        _ -> old++remapEdge :: M.Map Node Node -> LEdge CFEdge -> LEdge CFEdge+remapEdge map (from, to, label) = (remapHelper map from, remapHelper map to, label)+remapHelper map n = M.findWithDefault n n map++data Range = Range Node Node+  deriving (Eq, Show)++data CFContext = CFContext {+    cfIsCondition :: Bool,+    cfIsFunction :: Bool,+    cfLoopStack :: [(Node, Node)],+    cfTokenStack :: [Id],+    cfExitTarget :: Maybe Node,+    cfReturnTarget :: Maybe Node,+    cfParameters :: CFGParameters+}+newCFContext params = CFContext {+    cfIsCondition = False,+    cfIsFunction = False,+    cfLoopStack = [],+    cfTokenStack = [],+    cfExitTarget = Nothing,+    cfReturnTarget = Nothing,+    cfParameters = params+}++-- The monad we generate a graph in+type CFM a = RWS CFContext CFW Int a+type CFW = ([LNode CFNode], [LEdge CFEdge], [(Id, (Node, Node))], [(Id, Node)])++newNode :: CFNode -> CFM Node+newNode label = do+    n <- get+    stack <- asks cfTokenStack+    put (n+1)+    tell ([(n, label)], [], [], map (\c -> (c, n)) stack)+    return n++newNodeRange :: CFNode -> CFM Range+-- newNodeRange label = nodeToRange <$> newNode label+newNodeRange label = nodeToRange <$> newNode label++-- Build a disjoint piece of the graph and return a CFExecuteSubshell. The Id is used purely for debug naming.+subshell :: Id -> String -> CFM Range -> CFM Range+subshell id reason p = do+    start <- newNode $ CFEntryPoint $ "Subshell " ++ show id ++ ": " ++ reason+    end <- newNode CFStructuralNode+    middle <- local (\c -> c { cfExitTarget = Just end, cfReturnTarget = Just end}) p+    linkRanges [nodeToRange start, middle, nodeToRange end]+    newNodeRange $ CFExecuteSubshell reason start end+++withFunctionScope p = do+    end <- newNode CFStructuralNode+    body <- local (\c -> c { cfReturnTarget = Just end, cfIsFunction = True }) p+    linkRanges [body, nodeToRange end]++-- Anything that happens recursively in f will be attributed to this id+under :: Id -> CFM a -> CFM a+under id f = local (\c -> c { cfTokenStack = id:(cfTokenStack c) }) f++nodeToRange :: Node -> Range+nodeToRange n = Range n n++link :: Node -> Node -> CFEdge -> CFM ()+link from to label = do+    tell ([], [(from, to, label)], [], [])++registerNode :: Id -> Range -> CFM ()+registerNode id (Range start end) = tell ([], [], [(id, (start, end))], [])++linkRange :: Range -> Range -> CFM Range+linkRange = linkRangeAs CFEFlow++linkRangeAs :: CFEdge -> Range -> Range -> CFM Range+linkRangeAs label (Range start mid1) (Range mid2 end) = do+    link mid1 mid2 label+    return (Range start end)++-- Like linkRange but without actually linking+spanRange :: Range -> Range -> Range+spanRange (Range start mid1) (Range mid2 end) = Range start end++linkRanges :: [Range] -> CFM Range+linkRanges [] = error "Empty range"+linkRanges (first:rest) = foldM linkRange first rest++sequentially :: [Token] -> CFM Range+sequentially list = do+    first <- newStructuralNode+    rest <- mapM build list+    linkRanges (first:rest)++withContext :: (CFContext -> CFContext) -> CFM a -> CFM a+withContext = local++withReturn :: Range -> CFM a -> CFM a+withReturn _ p = p++asCondition :: CFM Range -> CFM Range+asCondition = withContext (\c -> c { cfIsCondition = True })++newStructuralNode = newNodeRange CFStructuralNode++buildRoot :: Token -> CFM Range+buildRoot t = under (getId t) $ do+    entry <- newNodeRange $ CFEntryPoint "MAIN"+    impliedExit <- newNode CFImpliedExit+    end <- newNode CFStructuralNode+    start <- local (\c -> c { cfExitTarget = Just end, cfReturnTarget = Just impliedExit}) $ build t+    range <- linkRanges [entry, start, nodeToRange impliedExit, nodeToRange end]+    registerNode (getId t) range+    return range++applySingle e = CFApplyEffects [e]++-- Build the CFG.+build :: Token -> CFM Range+build t = do+    range <- under (getId t) $ build' t+    registerNode (getId t) range+    return range+  where+    build' t = case t of+        T_Annotation _ _ list -> build list+        T_Script _ _ list -> do+            sequentially list++        TA_Assignment id op var@(TA_Variable _ name indices) rhs -> do+            -- value first: (( var[x=1] = (x=2) )) runs x=1 last+            value <- build rhs+            subscript <- sequentially indices+            read <-+                if op == "="+                then none+                -- This is += or something+                else newNodeRange $ applySingle $ IdTagged id $ CFReadVariable name++            write <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name $+                        if null indices+                        then CFValueInteger+                        else CFValueArray++            linkRanges [value, subscript, read, write]++        TA_Assignment id op lhs rhs -> do+            -- This is likely an invalid assignment like (( 1 = 2 )), but it+            -- could be e.g. x=y; (( $x = 3 )); echo $y, so expand both sides+            -- without updating anything+            sequentially [lhs, rhs]++        TA_Binary _ _ a b -> sequentially [a,b]+        TA_Expansion _ list -> sequentially list+        TA_Sequence _ list -> sequentially list+        TA_Parentesis _ t -> build t++        TA_Trinary _ cond a b -> do+            condition <- build cond+            ifthen <- build a+            elsethen <- build b+            end <- newStructuralNode+            linkRanges [condition, ifthen, end]+            linkRanges [condition, elsethen, end]++        TA_Variable id name indices -> do+            subscript <- sequentially indices+            hint <-+                if null indices+                then none+                else nodeToRange <$> newNode (applySingle $ IdTagged id $ CFHintArray name)+            read <- nodeToRange <$> newNode (applySingle $ IdTagged id $ CFReadVariable name)+            linkRanges [subscript, hint, read]++        TA_Unary id op (TA_Variable _ name indices) | "--" `isInfixOf` op || "++" `isInfixOf` op -> do+            subscript <- sequentially indices+            read <- newNodeRange $ applySingle $ IdTagged id $ CFReadVariable name+            write <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name $+                        if null indices+                        then CFValueInteger+                        else CFValueArray+            linkRanges [subscript, read, write]+        TA_Unary _ _ arg -> build arg++        TC_And _ SingleBracket _ lhs rhs -> do+            sequentially [lhs, rhs]++        TC_And _ DoubleBracket _ lhs rhs -> do+            left <- build lhs+            right <- build rhs+            end <- newStructuralNode+            -- complete+            linkRanges [left, right, end]+            -- short circuit+            linkRange left end++        -- TODO: Handle integer ops+        TC_Binary _ mode str lhs rhs -> do+            left <- build lhs+            right <- build rhs+            linkRange left right++        TC_Empty {} -> newStructuralNode++        TC_Group _ _ t -> build t++        -- TODO: Mark as checked+        TC_Nullary _ _ arg -> build arg++        TC_Or _ SingleBracket _ lhs rhs -> sequentially [lhs, rhs]++        TC_Or _ DoubleBracket _ lhs rhs -> do+            left <- build lhs+            right <- build rhs+            end <- newStructuralNode+            -- complete+            linkRanges [left, right, end]+            -- short circuit+            linkRange left end++        -- TODO: Handle -v, -z, -n+        TC_Unary _ _ op arg -> do+            build arg++        T_Arithmetic id root -> do+            exe <- build root+            status <- newNodeRange (CFSetExitCode id)+            linkRange exe status++        T_AndIf _ lhs rhs -> do+            left <- build lhs+            right <- build rhs+            end <- newStructuralNode+            linkRange left right+            linkRange right end+            linkRange left end++        T_Array _ list -> sequentially list++        T_Assignment {} -> buildAssignment DefaultScope t++        T_Backgrounded id body -> do+            start <- newStructuralNode+            fork <- subshell id "backgrounding '&'" $ build body+            pid <- newNodeRange $ CFSetBackgroundPid id+            status <- newNodeRange $ CFSetExitCode id++            linkRange start fork+            -- Add a join from the fork to warn about variable changes+            linkRangeAs CFEFalseFlow fork pid+            linkRanges [start, pid, status]++        T_Backticked id body ->+            subshell id "`..` expansion" $ sequentially body++        T_Banged id cmd -> do+            main <- build cmd+            status <- newNodeRange (CFSetExitCode id)+            linkRange main status++        T_BatsTest id _ body -> do+            -- These are technically set by the 'run' command, but we'll just define them+            -- up front to avoid figuring out which commands named "run" belong to Bats.+            status <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable "status" CFValueInteger+            output <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable "output" CFValueString+            main <- build body+            linkRanges [status, output, main]++        T_BraceExpansion _ list -> sequentially list++        T_BraceGroup id body ->+            sequentially body++        T_CaseExpression id t [] -> build t++        T_CaseExpression id t list -> do+            start <- newStructuralNode+            token <- build t+            branches <- mapM buildBranch list+            end <- newStructuralNode++            let neighbors = zip branches $ tail branches+            let (_, firstCond, _) = head branches+            let (_, lastCond, lastBody) = last branches++            linkRange start token+            linkRange token firstCond+            mapM_ (uncurry $ linkBranch end) neighbors+            linkRange lastBody end++            unless (any hasCatchAll list) $+                -- There's no *) branch, so assume we can fall through+                void $ linkRange token end++            return $ spanRange start end++          where+            -- for a | b | c, evaluate each in turn and allow short circuiting+            buildCond list = do+                start <- newStructuralNode+                conds <- mapM build list+                end <- newStructuralNode+                linkRanges (start:conds)+                mapM_ (`linkRange` end) conds+                return $ spanRange start end++            buildBranch (typ, cond, body) = do+                c <- buildCond cond+                b <- sequentially body+                linkRange c b+                return (typ, c, b)++            linkBranch end (typ, cond, body) (_, nextCond, nextBody) = do+                -- Failure case+                linkRange cond nextCond+                -- After body+                case typ of+                    CaseBreak -> linkRange body end+                    CaseFallThrough -> linkRange body nextBody+                    CaseContinue -> linkRange body nextCond++            -- Find a *) if any++            hasCatchAll (_,cond,_) = any isCatchAll cond+            isCatchAll c = fromMaybe False $ do+                pg <- wordToExactPseudoGlob c+                return $ pg `pseudoGlobIsSuperSetof` [PGMany]++        T_Condition id _ op -> do+            cond <- build op+            status <- newNodeRange $ CFSetExitCode id+            linkRange cond status++        T_CoProc id maybeName t -> do+            let name = fromMaybe "COPROC" maybeName+            start <- newStructuralNode+            parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray+            child <- subshell id "coproc" $ build t+            end <- newNodeRange $ CFSetExitCode id++            linkRange start parent+            linkRange start child+            linkRange parent end+            linkRangeAs CFEFalseFlow child end++            return $ spanRange start end+        T_CoProcBody _ t -> build t++        T_DollarArithmetic _ arith -> build arith+        T_DollarDoubleQuoted _ list -> sequentially list+        T_DollarSingleQuoted _ _ -> none+        T_DollarBracket _ t -> build t++        T_DollarBraced id _ t -> do+            let str = concat $ oversimplify t+            let modifier = getBracedModifier str+            let reference = getBracedReference str+            let indices = getIndexReferences str+            let offsets = getOffsetReferences str+            vals <- build t+            others <- mapM (\x -> nodeToRange <$> newNode (applySingle $ IdTagged id $ CFReadVariable x)) (indices ++ offsets)+            deps <- linkRanges (vals:others)+            read <- nodeToRange <$> newNode (applySingle $ IdTagged id $ CFReadVariable reference)+            totalRead <- linkRange deps read++            if any (`isPrefixOf` modifier) ["=", ":="]+              then do+                optionalAssign <- newNodeRange (applySingle $ IdTagged id $ CFWriteVariable reference CFValueString)+                result <- newStructuralNode+                linkRange optionalAssign result+                linkRange totalRead result+              else return totalRead++        T_DoubleQuoted _ list -> sequentially list++        T_DollarExpansion id body ->+            subshell id "$(..) expansion" $ sequentially body++        T_Extglob _ _ list -> sequentially list++        T_FdRedirect id ('{':identifier) op -> do+            let name = takeWhile (/= '}') identifier+            expression <- build op+            rw <- newNodeRange $+                if isClosingFileOp op+                then applySingle $ IdTagged id $ CFReadVariable name+                else applySingle $ IdTagged id $ CFWriteVariable name CFValueInteger++            linkRange expression rw+++        T_FdRedirect _ name t -> do+            build t++        T_ForArithmetic _ initT condT incT bodyT -> do+            init <- build initT+            cond <- build condT+            body <- sequentially bodyT+            inc <- build incT+            end <- newStructuralNode++            -- Forward edges+            linkRanges [init, cond, body, inc]+            linkRange cond end+            -- Backward edge+            linkRange inc cond+            return $ spanRange init end++        T_ForIn id name words body -> forInHelper id name words body++        -- For functions we generate an unlinked subgraph, and mention that in its definition node+        T_Function id _ _ name body -> do+            range <- local (\c -> c { cfExitTarget = Nothing }) $ do+                entry <- newNodeRange $ CFEntryPoint $ "function " ++ name+                f <- withFunctionScope $ build body+                linkRange entry f+            let (Range entry exit) = range+            definition <- newNodeRange (applySingle $ IdTagged id $ CFDefineFunction name id entry exit)+            exe <- newNodeRange (CFSetExitCode id)+            linkRange definition exe++        T_Glob {} -> none++        T_HereString _ t -> build t+        T_HereDoc _ _ _ _ list -> sequentially list++        T_IfExpression id ifs elses -> do+            start <- newStructuralNode+            branches <- doBranches start ifs elses []+            end <- newStructuralNode+            mapM_ (`linkRange` end) branches+            return $ spanRange start end+          where+            doBranches start ((conds, thens):rest) elses result = do+                cond <- asCondition $ sequentially conds+                action <- sequentially thens+                linkRange start cond+                linkRange cond action+                doBranches cond rest elses (action:result)+            doBranches start [] elses result = do+                rest <-+                    if null elses+                    then newNodeRange (CFSetExitCode id)+                    else sequentially elses+                linkRange start rest+                return (rest:result)++        T_Include _ t -> build t++        T_IndexedElement _ indicesT valueT -> do+            indices <- sequentially indicesT+            value <- build valueT+            linkRange indices value++        T_IoDuplicate _ op _ -> build op++        T_IoFile _ op t -> do+            exp <- build t+            doesntDoMuch <- build op+            linkRange exp doesntDoMuch++        T_Literal {} -> none++        T_NormalWord _ list -> sequentially list++        T_OrIf _ lhs rhs -> do+            left <- build lhs+            right <- build rhs+            end <- newStructuralNode+            linkRange left right+            linkRange right end+            linkRange left end++        T_Pipeline _ _ [cmd] -> build cmd+        T_Pipeline id _ cmds -> do+            start <- newStructuralNode+            hasLastpipe <- reader $ cfLastpipe . cfParameters+            (leading, last) <- buildPipe hasLastpipe cmds+            -- Ideally we'd let this exit code be that of the last command in the pipeline but ok+            end <- newNodeRange $ CFSetExitCode id++            mapM_ (linkRange start) leading+            mapM_ (\c -> linkRangeAs CFEFalseFlow c end) leading+            linkRanges $ [start] ++ last ++ [end]+          where+            buildPipe True [x] = do+                last <- build x+                return ([], [last])+            buildPipe lp (first:rest) = do+                this <- subshell id "pipeline" $ build first+                (leading, last) <- buildPipe lp rest+                return (this:leading, last)+            buildPipe _ [] = return ([], [])++        T_ProcSub id op cmds -> do+            start <- newStructuralNode+            body <- subshell id (op ++ "() process substitution") $ sequentially cmds+            end <- newStructuralNode++            linkRange start body+            linkRangeAs CFEFalseFlow body end+            linkRange start end++        T_Redirecting _ redirs cmd -> do+            -- For simple commands, this is the other way around in bash+            -- We do it in this order for comound commands like { x=name; } > "$x"+            redir <- sequentially redirs+            body <- build cmd+            linkRange redir body++        T_SelectIn id name words body -> forInHelper id name words body++        T_SimpleCommand id vars [] -> do+            -- Vars can also be empty, as in the command "> foo"+            assignments <- sequentially vars+            status <- newNodeRange (CFSetExitCode id)+            linkRange assignments status++        T_SimpleCommand id vars list@(cmd:_) ->+            handleCommand t vars list $ getUnquotedLiteral cmd++        T_SingleQuoted _ _ -> none++        T_SourceCommand _ originalCommand inlinedSource -> do+            cmd <- build originalCommand+            end <- newStructuralNode+            inline <- withReturn end $ build inlinedSource+            linkRange cmd inline+            linkRange inline end+            return $ spanRange cmd inline++        T_Subshell id body -> do+            main <- subshell id "explicit (..) subshell" $ sequentially body+            status <- newNodeRange (CFSetExitCode id)+            linkRange main status++        T_UntilExpression id cond body -> whileHelper id cond body+        T_WhileExpression id cond body -> whileHelper id cond body++        T_CLOBBER _ -> none+        T_GREATAND _ -> none+        T_LESSAND _ -> none+        T_LESSGREAT _ -> none+        T_DGREAT _ -> none+        T_Greater _ -> none+        T_Less _ -> none+        T_ParamSubSpecialChar _ _ -> none++        x -> error ("Unimplemented: " ++ show x)++--  Still in `where` clause+    forInHelper id name words body = do+        entry <- newStructuralNode+        expansion <- sequentially words+        assignmentChoice <- newStructuralNode+        assignments <-+            if null words || any willSplit words+            then (:[]) <$> (newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueString)+            else mapM (\t -> newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name $ CFValueComputed (getId t) $ tokenToParts t) words+        body <- sequentially body+        exit <- newStructuralNode+        -- Forward edges+        linkRanges [entry, expansion, assignmentChoice]+        mapM_ (\t -> linkRanges [assignmentChoice, t, body]) assignments+        linkRange body exit+        linkRange expansion exit+        -- Backward edge+        linkRange body assignmentChoice+        return $ spanRange entry exit++    whileHelper id cond body = do+        condRange <- asCondition $ sequentially cond+        bodyRange <- sequentially body+        end <- newNodeRange (CFSetExitCode id)++        linkRange condRange bodyRange+        linkRange bodyRange condRange+        linkRange condRange end+++handleCommand cmd vars args literalCmd = do+    -- TODO: Handle assignments in declaring commands++    case literalCmd of+        Just "exit" -> regularExpansion vars args $ handleExit+        Just "return" -> regularExpansion vars args $ handleReturn+        Just "unset" -> regularExpansionWithStatus vars args $ handleUnset args++        Just "declare" -> handleDeclare args+        Just "local" -> handleDeclare args+        Just "typeset" -> handleDeclare args++        Just "printf" -> regularExpansionWithStatus vars args $ handlePrintf args+        Just "wait" -> regularExpansionWithStatus vars args $ handleWait args++        Just "mapfile" -> regularExpansionWithStatus vars args $ handleMapfile args+        Just "readarray" -> regularExpansionWithStatus vars args $ handleMapfile args++        Just "read" -> regularExpansionWithStatus vars args $ handleRead args++        Just "DEFINE_boolean" -> regularExpansionWithStatus vars args $ handleDEFINE args+        Just "DEFINE_float" ->   regularExpansionWithStatus vars args $ handleDEFINE args+        Just "DEFINE_integer" -> regularExpansionWithStatus vars args $ handleDEFINE args+        Just "DEFINE_string" ->  regularExpansionWithStatus vars args $ handleDEFINE args++        -- This will mostly behave like 'command' but ok+        Just "builtin" ->+            case args of+                [_] -> regular+                (_:newargs@(newcmd:_)) ->+                    handleCommand newcmd vars newargs $ getLiteralString newcmd+        Just "command" ->+            case args of+                [_] -> regular+                (_:newargs@(newcmd:_)) ->+                    handleOthers (getId newcmd) vars newargs $ getLiteralString newcmd+        _ -> regular++  where+    regular = handleOthers (getId cmd) vars args literalCmd+    handleExit = do+        exitNode <- reader cfExitTarget+        case exitNode of+            Just target -> do+                exit <- newNode CFResolvedExit+                link exit target CFEExit+                unreachable <- newNode CFUnreachable+                return $ Range exit unreachable+            Nothing -> do+                exit <- newNode CFUnresolvedExit+                unreachable <- newNode CFUnreachable+                return $ Range exit unreachable++    handleReturn = do+        returnTarget <- reader cfReturnTarget+        case returnTarget of+            Nothing -> error $ pleaseReport "missing return target"+            Just target -> do+                ret <- newNode CFStructuralNode+                link ret target CFEFlow+                unreachable <- newNode CFUnreachable+                return $ Range ret unreachable++    handleUnset (cmd:args) = do+        case () of+                _ | "n" `elem` flagNames -> unsetWith CFUndefineNameref+                _ | "v" `elem` flagNames -> unsetWith CFUndefineVariable+                _ | "f" `elem` flagNames -> unsetWith CFUndefineFunction+                _ -> unsetWith CFUndefine+      where+        pairs :: [(String, Token)] -- [(Flag string, token)] e.g. [("-f", t), ("", myfunc)]+        pairs = map (\(str, (flag, val)) -> (str, flag)) $ fromMaybe (map (\c -> ("", (c,c))) args) $ getGnuOpts "vfn" args+        (names, flags) = partition (null . fst) pairs+        flagNames = map fst flags+        literalNames :: [(Token, String)] -- Literal names to unset, e.g. [(myfuncToken, "myfunc")]+        literalNames = mapMaybe (\(_, t) -> getLiteralString t >>= (return . (,) t)) names+        -- Apply a constructor like CFUndefineVariable to each literalName, and tag with its id+        unsetWith c = newNodeRange $ CFApplyEffects $ map (\(token, name) -> IdTagged (getId token) $ c name) literalNames+++    variableAssignRegex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)="++    handleDeclare (cmd:args) = do+        isFunc <- asks cfIsFunction+        -- This is a bit of a kludge: we don't have great support for things like+        -- 'declare -i x=$x' so do one round with declare x=$x, followed by declare -i x+        let (evaluated, assignments, added, removed) = mconcat $ map (toEffects isFunc) args+        before <- sequentially $ evaluated+        assignments <- newNodeRange $ CFApplyEffects assignments+        addedProps <- if null added then newStructuralNode else newNodeRange $ CFApplyEffects added+        removedProps <- if null removed then newStructuralNode else newNodeRange $ CFApplyEffects removed+        result <- newNodeRange $ CFSetExitCode (getId cmd)+        linkRanges [before, assignments, addedProps, removedProps, result]+      where+        opts = map fst $ getGenericOpts args+        array = "a" `elem` opts || associative+        associative = "A" `elem` opts+        integer = "i" `elem` opts+        func = "f" `elem` opts || "F" `elem` opts+        global = "g" `elem` opts+        export = "x" `elem` opts+        writer isFunc =+            case () of+                _ | global -> CFWriteGlobal+                _ | isFunc -> CFWriteLocal+                _ -> CFWriteVariable++        scope isFunc =+            case () of+                _ | global -> GlobalScope+                _ | isFunc -> LocalScope+                _ -> DefaultScope++        addedProps = S.fromList $ concat $ [+            [ CFVPArray | array ],+            [ CFVPInteger | integer ],+            [ CFVPExport | export ],+            [ CFVPAssociative | associative ]+          ]++        removedProps = S.fromList $ concat $ [+            -- Array property can't be unset+            [ CFVPInteger | 'i' `elem` unsetOptions ],+            [ CFVPExport | 'e' `elem` unsetOptions ]+          ]++        toEffects isFunc (T_Assignment id mode var idx t) =+            let+                pre = idx ++ [t]+                val = [ IdTagged id $ (writer isFunc) var $ CFValueComputed (getId t) $ [ CFStringVariable var | mode == Append ] ++ tokenToParts t ]+                added = [ IdTagged id $ CFSetProps (scope isFunc) var addedProps | not $ S.null addedProps ]+                removed = [ IdTagged id $ CFUnsetProps (scope isFunc) var addedProps | not $ S.null removedProps ]+            in+                (pre, val, added, removed)++        toEffects isFunc t =+            let+                id = getId t+                pre = [t]+                literal = fromJust $ getLiteralStringExt (const $ Just "\0") t+                isKnown = '\0' `notElem` literal+                match = fmap head $ variableAssignRegex `matchRegex` literal+                name = fromMaybe literal match++                asLiteral =+                    IdTagged id $ (writer isFunc) name $+                        CFValueComputed (getId t) [ CFStringLiteral $ drop 1 $ dropWhile (/= '=') $ literal ]+                asUnknown =+                    IdTagged id $ (writer isFunc) name $+                        CFValueString++                added = [ IdTagged id $ CFSetProps (scope isFunc) name addedProps ]+                removed = [ IdTagged id $ CFUnsetProps (scope isFunc) name removedProps ]++            in+                case () of+                    _ | not (isVariableName name) -> (pre, [], [], [])+                    _ | isJust match && isKnown -> (pre, [asLiteral], added, removed)+                    _ | isJust match -> (pre, [asUnknown], added, removed)+                    -- e.g. declare -i x+                    _ -> (pre, [], added, removed)++        -- find "ia" from `define +i +a`+        unsetOptions :: String+        unsetOptions =+            let+                strings = mapMaybe getLiteralString args+                plusses = filter ("+" `isPrefixOf`) strings+            in+                concatMap (drop 1) plusses++    handlePrintf (cmd:args) =+        newNodeRange $ CFApplyEffects $ maybeToList findVar+      where+        findVar = do+            flags <- getBsdOpts "v:" args+            (flag, arg) <- lookup "v" flags+            name <- getLiteralString arg+            return $ IdTagged (getId arg) $ CFWriteVariable name CFValueString++    handleWait (cmd:args) =+        newNodeRange $ CFApplyEffects $ maybeToList findVar+      where+        findVar = do+            let flags = getGenericOpts args+            (flag, arg) <- lookup "p" flags+            name <- getLiteralString arg+            return $ IdTagged (getId arg) $ CFWriteVariable name CFValueInteger++    handleMapfile (cmd:args) =+        newNodeRange $ CFApplyEffects [findVar]+      where+        findVar =+            let (id, name) = fromMaybe (getId cmd, "MAPFILE") $ getFromArg `mplus` getFromFallback+            in IdTagged id $ CFWriteVariable name CFValueArray++        getFromArg = do+            flags <- getGnuOpts flagsForMapfile args+            (_, arg) <- lookup "" flags+            name <- getLiteralString arg+            return (getId arg, name)++        getFromFallback =+            listToMaybe $ mapMaybe getIfVar $ reverse args+        getIfVar c = do+            name <- getLiteralString c+            guard $ isVariableName name+            return (getId c, name)++    handleRead (cmd:args) = newNodeRange $ CFApplyEffects main+      where+        main = fromMaybe fallback $ do+            flags <- getGnuOpts flagsForRead args+            return $ fromMaybe (withFields flags) $ withArray flags++        withArray :: [(String, (Token, Token))] -> Maybe [IdTagged CFEffect]+        withArray flags = do+            (_, token) <- lookup "a" flags+            return $ fromMaybe [] $ do+                name <- getLiteralString token+                return [ IdTagged (getId token) $ CFWriteVariable name CFValueArray ]++        withFields flags = mapMaybe getAssignment flags++        getAssignment :: (String, (Token, Token)) -> Maybe (IdTagged CFEffect)+        getAssignment f = do+            ("", (t, _)) <- return f+            name <- getLiteralString t+            return $ IdTagged (getId t) $ CFWriteVariable name CFValueString++        fallback =+            let+                names = reverse $ map fromJust $ takeWhile isJust $ map (\c -> sequence (getId c, getLiteralString c)) $ reverse args+                namesOrDefault = if null names then [(getId cmd, "REPLY")] else names+                hasDashA = any (== "a") $ map fst $ getGenericOpts args+                value = if hasDashA then CFValueArray else CFValueString+            in+                map (\(id, name) -> IdTagged id $ CFWriteVariable name value) namesOrDefault++    handleDEFINE (cmd:args) =+        newNodeRange $ CFApplyEffects $ maybeToList findVar+      where+        findVar = do+            name <- listToMaybe $ drop 1 args+            str <- getLiteralString name+            guard $ isVariableName str+            return $ IdTagged (getId name) $ CFWriteVariable str CFValueString++    handleOthers id vars args cmd =+        regularExpansion vars args $ do+            exe <- newNodeRange $ CFExecuteCommand cmd+            status <- newNodeRange $ CFSetExitCode id+            linkRange exe status++    regularExpansion vars args p = do+            args <- sequentially args+            assignments <- mapM (buildAssignment PrefixScope) vars+            exe <- p+            dropAssignments <-+                if null vars+                then+                    return []+                else do+                    drop <- newNodeRange CFDropPrefixAssignments+                    return [drop]++            linkRanges $ [args] ++ assignments ++ [exe] ++ dropAssignments++    regularExpansionWithStatus vars args@(cmd:_) p = do+        initial <- regularExpansion vars args p+        status <- newNodeRange $ CFSetExitCode (getId cmd)+        linkRange initial status+++none = newStructuralNode++data Scope = DefaultScope | GlobalScope | LocalScope | PrefixScope+  deriving (Eq, Ord, Show, Generic, NFData)++buildAssignment scope t = do+    op <- case t of+            T_Assignment id mode var indices value -> do+                expand <- build value+                index <- sequentially indices+                read <- case mode of+                    Append -> newNodeRange (applySingle $ IdTagged id $ CFReadVariable var)+                    Assign -> none+                let valueType = if null indices then f id value else CFValueArray+                let scoper =+                                case scope of+                                    PrefixScope -> CFWritePrefix+                                    LocalScope -> CFWriteLocal+                                    GlobalScope -> CFWriteGlobal+                                    DefaultScope -> CFWriteVariable+                write <- newNodeRange $ applySingle $ IdTagged id $ scoper var valueType+                linkRanges [expand, index, read, write]+              where+                f :: Id -> Token -> CFValue+                f id t@T_NormalWord {} = CFValueComputed id $ [CFStringVariable var | mode == Append] ++ tokenToParts t+                f id t@(T_Literal _ str) = CFValueComputed id $ [CFStringVariable var | mode == Append] ++ tokenToParts t+                f _ T_Array {} = CFValueArray++    registerNode (getId t) op+    return op+++tokenToParts t =+    case t of+        T_NormalWord _ list -> concatMap tokenToParts list+        T_DoubleQuoted _ list -> concatMap tokenToParts list+        T_SingleQuoted _ str -> [ CFStringLiteral str ]+        T_Literal _ str -> [ CFStringLiteral str ]+        T_DollarArithmetic {} -> [ CFStringInteger ]+        T_DollarBracket {} -> [ CFStringInteger ]+        T_DollarBraced _ _ list | isUnmodifiedParameterExpansion t -> [ CFStringVariable (getBracedReference $ concat $ oversimplify list) ]+        -- Check if getLiteralString can handle it, if not it's unknown+        _ -> [maybe CFStringUnknown CFStringLiteral $ getLiteralString t]+++-- Like & but well defined when the node already exists+safeUpdate ctx@(_,node,_,_) graph = ctx & (delNode node graph)++-- Change all subshell invocations to instead link directly to their contents.+-- This is used for producing dominator trees.+inlineSubshells :: CFGraph -> CFGraph+inlineSubshells graph = relinkedGraph+  where+    subshells = ufold find [] graph+    find (incoming, node, label, outgoing) acc =+        case label of+            CFExecuteSubshell _ start end -> (node, label, start, end, incoming, outgoing):acc+            _ -> acc++    relinkedGraph = foldl' relink graph subshells+    relink graph (node, label, start, end, incoming, outgoing) =+        let+            -- Link CFExecuteSubshell to the CFEntryPoint+            subshellToStart = (incoming, node, label, [(CFEFlow, start)])+            -- Link the subshell exit to the+            endToNexts = (endIncoming, endNode, endLabel, outgoing)+            (endIncoming, endNode, endLabel, _) = context graph end+        in+            subshellToStart `safeUpdate` (endToNexts `safeUpdate` graph)++findEntryNodes :: CFGraph -> [Node]+findEntryNodes graph = ufold find [] graph+  where+    find (incoming, node, label, _) list =+        case label of+            CFEntryPoint {} | null incoming -> node:list+            _ -> list++findDominators main graph = asSetMap+  where+    inlined = inlineSubshells graph+    entryNodes = main : findEntryNodes graph+    asLists = concatMap (dom inlined) entryNodes+    asSetMap = M.fromList $ map (\(node, list) -> (node, S.fromList list)) asLists++findTerminalNodes :: CFGraph -> [Node]+findTerminalNodes graph = ufold find [] graph+  where+    find (_, node, label, _) list =+        case label of+            CFUnresolvedExit -> node:list+            CFApplyEffects effects -> f effects list+            _ -> list++    f [] list = list+    f (IdTagged _ (CFDefineFunction _ id start end):rest) list = f rest (end:list)+    f (_:rest) list = f rest list++findPostDominators :: Node -> CFGraph -> Array Node [Node]+findPostDominators mainexit graph = asArray+  where+    inlined = inlineSubshells graph+    terminals = findTerminalNodes inlined+    (incoming, _, label, outgoing) = context graph mainexit+    withExitEdges = (incoming ++ map (\c -> (CFEFlow, c)) terminals, mainexit, label, outgoing) `safeUpdate` inlined+    reversed = grev withExitEdges+    postDoms = dom reversed mainexit+    (_, maxNode) = nodeRange graph+    asArray = array (0, maxNode) postDoms++return []+runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
+ src/ShellCheck/CFGAnalysis.hs view
@@ -0,0 +1,1424 @@+{-+    Copyright 2022 Vidar Holen++    This file is part of ShellCheck.+    https://www.shellcheck.net++    ShellCheck is free software: you can redistribute it and/or modify+    it under the terms of the GNU General Public License as published by+    the Free Software Foundation, either version 3 of the License, or+    (at your option) any later version.++    ShellCheck is distributed in the hope that it will be useful,+    but WITHOUT ANY WARRANTY; without even the implied warranty of+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+    GNU General Public License for more details.++    You should have received a copy of the GNU General Public License+    along with this program.  If not, see <https://www.gnu.org/licenses/>.+-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-}+{-# LANGUAGE CPP #-}++{-+    Data Flow Analysis on a Control Flow Graph.++    This module implements a pretty standard iterative Data Flow Analysis.+    For an overview of the process, see Wikipedia.++    Since shell scripts rely heavily on global variables, this DFA includes+    tracking the value of globals across calls. Each function invocation is+    treated as a separate DFA problem, and a caching mechanism (hopefully)+    avoids any exponential explosions.++    To do efficient DFA join operations (or merges, as the code calls them),+    some of the data structures have an integer version attached. On update,+    the version is changed. If two states have the same version number,+    a merge is skipped on the grounds that they are identical. It is easy+    to unintentionally forget to update/invalidate the version number,+    and bugs will ensure.++    For performance reasons, the entire code runs in plain ST, with a manual+    context object Ctx being passed around. It relies heavily on mutable+    STRefs. However, this turned out to be literally thousands of times faster+    than my several attempts using RWST, so it can't be helped.+-}++module ShellCheck.CFGAnalysis (+    analyzeControlFlow+    ,CFGParameters (..)+    ,CFGAnalysis (..)+    ,ProgramState (..)+    ,VariableState (..)+    ,VariableValue (..)+    ,VariableProperties+    ,SpaceStatus (..)+    ,NumericalStatus (..)+    ,getIncomingState+    ,getOutgoingState+    ,doesPostDominate+    ,ShellCheck.CFGAnalysis.runTests -- STRIP+    ) where++import Control.DeepSeq+import Control.Monad+import Control.Monad.ST+import Data.Array.Unboxed+import Data.Char+import Data.Graph.Inductive.Graph+import Data.Graph.Inductive.Query.DFS+import Data.List hiding (map)+import Data.Maybe+import Data.STRef+import Debug.Trace -- STRIP+import GHC.Generics (Generic)+import qualified Data.Map as M+import qualified Data.Set as S+import qualified ShellCheck.Data as Data+import ShellCheck.AST+import ShellCheck.CFG+import ShellCheck.Prelude++import Test.QuickCheck+++-- The number of iterations for DFA to stabilize+iterationCount = 1000000+-- There have been multiple bugs where bad caching caused oscillations.+-- As a precaution, disable caching if there's this many iterations left.+fallbackThreshold = 10000+-- The number of cache entries to keep per node+cacheEntries = 10++logVerbose log = do+    -- traceShowM log+    return ()+logInfo log = do+    -- traceShowM log+    return ()++-- The result of the data flow analysis+data CFGAnalysis = CFGAnalysis {+    graph :: CFGraph,+    tokenToRange :: M.Map Id (Node, Node),+    tokenToNodes :: M.Map Id (S.Set Node),+    postDominators :: Array Node [Node],+    nodeToData :: M.Map Node (ProgramState, ProgramState)+} deriving (Show)++-- The program state we expose externally+data ProgramState = ProgramState {+    -- internalState :: InternalState, -- For debugging+    variablesInScope :: M.Map String VariableState,+    exitCodes :: S.Set Id,+    stateIsReachable :: Bool+} deriving (Show, Eq, Generic, NFData)++internalToExternal :: InternalState -> ProgramState+internalToExternal s =+    ProgramState {+        -- Censor the literal value to avoid introducing dependencies on it. It's just for debugging.+        variablesInScope = M.map censor flatVars,+        -- internalState = s, -- For debugging+        exitCodes = fromMaybe S.empty $ sExitCodes s,+        stateIsReachable = fromMaybe True $ sIsReachable s+    }+  where+    censor s = s {+        variableValue = (variableValue s) {+            literalValue = Nothing+        }+    }+    flatVars = M.unionsWith (\_ last -> last) $ map mapStorage [sGlobalValues s, sLocalValues s, sPrefixValues s]++-- Conveniently get the state before a token id+getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState+getIncomingState analysis id = do+    (start,end) <- M.lookup id $ tokenToRange analysis+    fst <$> M.lookup start (nodeToData analysis)++-- Conveniently get the state after a token id+getOutgoingState :: CFGAnalysis -> Id -> Maybe ProgramState+getOutgoingState analysis id = do+    (start,end) <- M.lookup id $ tokenToRange analysis+    snd <$> M.lookup end (nodeToData analysis)++-- Conveniently determine whether one node postdominates another,+-- i.e. whether 'target' always unconditionally runs after 'base'.+doesPostDominate :: CFGAnalysis -> Id -> Id -> Bool+doesPostDominate analysis target base = fromMaybe False $ do+    (_, baseEnd) <- M.lookup base $ tokenToRange analysis+    (targetStart, _) <- M.lookup target $ tokenToRange analysis+    return $ targetStart `elem` (postDominators analysis ! baseEnd)++getDataForNode analysis node = M.lookup node $ nodeToData analysis++-- The current state of data flow at a point in the program, potentially as a diff+data InternalState = InternalState {+    sVersion :: Integer,+    sGlobalValues :: VersionedMap String VariableState,+    sLocalValues :: VersionedMap String VariableState,+    sPrefixValues :: VersionedMap String VariableState,+    sFunctionTargets :: VersionedMap String FunctionValue,+    sExitCodes :: Maybe (S.Set Id),+    sIsReachable :: Maybe Bool+} deriving (Show, Generic, NFData)++newInternalState = InternalState {+    sVersion = 0,+    sGlobalValues = vmEmpty,+    sLocalValues = vmEmpty,+    sPrefixValues = vmEmpty,+    sFunctionTargets = vmEmpty,+    sExitCodes = Nothing,+    sIsReachable = Nothing+}++unreachableState = modified newInternalState {+    sIsReachable = Just False+}++-- The default state we assume we get from the environment+createEnvironmentState :: InternalState+createEnvironmentState = do+    foldl' (flip ($)) newInternalState $ concat [+        addVars Data.internalVariables unknownVariableState,+        addVars Data.variablesWithoutSpaces spacelessVariableState,+        addVars Data.specialIntegerVariables integerVariableState+        ]+  where+    addVars names val = map (\name -> insertGlobal name val) names+    spacelessVariableState = unknownVariableState {+        variableValue = VariableValue {+            literalValue = Nothing,+            spaceStatus = SpaceStatusClean,+            numericalStatus = NumericalStatusUnknown+        }+    }+    integerVariableState = unknownVariableState {+        variableValue = unknownIntegerValue+    }+++modified s = s { sVersion = -1 }++insertGlobal :: String -> VariableState -> InternalState -> InternalState+insertGlobal name value state = modified state {+    sGlobalValues = vmInsert name value $ sGlobalValues state+}++insertLocal :: String -> VariableState -> InternalState -> InternalState+insertLocal name value state = modified state {+    sLocalValues = vmInsert name value $ sLocalValues state+}++insertPrefix :: String -> VariableState -> InternalState -> InternalState+insertPrefix name value state = modified state {+    sPrefixValues = vmInsert name value $ sPrefixValues state+}++insertFunction :: String -> FunctionValue -> InternalState -> InternalState+insertFunction name value state = modified state {+    sFunctionTargets = vmInsert name value $ sFunctionTargets state+}++addProperties :: S.Set CFVariableProp -> VariableState -> VariableState+addProperties props state = state {+    variableProperties = S.map (S.union props) $ variableProperties state+}++removeProperties :: S.Set CFVariableProp -> VariableState -> VariableState+removeProperties props state = state {+    variableProperties = S.map (\s -> S.difference s props) $ variableProperties state+}++setExitCode id = setExitCodes (S.singleton id)+setExitCodes set state = modified state {+    sExitCodes = Just $ set+}++-- Dependencies on values, e.g. "if there is a global variable named 'foo' without spaces"+-- This is used to see if the DFA of a function would result in the same state, so anything+-- that affects DFA must be tracked.+data StateDependency =+    -- Complete variable state+    DepState Scope String VariableState+    -- Only variable properties (we need properties but not values for x=1)+    | DepProperties Scope String VariableProperties+    -- Function definition+    | DepFunction String (S.Set FunctionDefinition)+    -- Whether invoking the node would result in recursion (i.e., is the function on the stack?)+    | DepIsRecursive Node Bool+    -- The set of commands that could have provided the exit code $?+    | DepExitCodes (S.Set Id)+    deriving (Show, Eq, Ord, Generic, NFData)++-- A function definition, or lack thereof+data FunctionDefinition = FunctionUnknown | FunctionDefinition String Node Node+    deriving (Show, Eq, Ord, Generic, NFData)++-- The Set of places a command name can point (it's a Set to handle conditionally defined functions)+type FunctionValue = S.Set FunctionDefinition++-- Create an InternalState that fulfills the given dependencies+depsToState :: S.Set StateDependency -> InternalState+depsToState set = foldl insert newInternalState $ S.toList set+  where+    insert :: InternalState -> StateDependency -> InternalState+    insert state dep =+        case dep of+            DepFunction name val -> insertFunction name val state+            DepState scope name val -> insertIn True scope name val state+            -- State includes properties and more, so don't overwrite a state with properties+            DepProperties scope name props -> insertIn False scope name unknownVariableState { variableProperties = props } state+            DepIsRecursive _ _ -> state+            DepExitCodes s -> setExitCodes s state++    insertIn overwrite scope name val state =+        let+            (mapToCheck, inserter) =+                case scope of+                    PrefixScope -> (sPrefixValues, insertPrefix)+                    LocalScope -> (sLocalValues, insertLocal)+                    GlobalScope -> (sGlobalValues, insertGlobal)+                    DefaultScope -> error $ pleaseReport "Unresolved scope in dependency"++            alreadyExists = isJust $ vmLookup name $ mapToCheck state+        in+            if overwrite || not alreadyExists+            then inserter name val state+            else state++unknownFunctionValue = S.singleton FunctionUnknown++-- The information about the value of a single variable+data VariableValue = VariableValue {+    literalValue :: Maybe String, -- TODO: For debugging. Remove me.+    spaceStatus :: SpaceStatus,+    numericalStatus :: NumericalStatus+}+    deriving (Show, Eq, Ord, Generic, NFData)++data VariableState = VariableState {+    variableValue :: VariableValue,+    variableProperties :: VariableProperties+}+    deriving (Show, Eq, Ord, Generic, NFData)++-- Whether or not the value needs quoting (has spaces/globs), or we don't know+data SpaceStatus = SpaceStatusEmpty | SpaceStatusClean | SpaceStatusDirty deriving (Show, Eq, Ord, Generic, NFData)+--+-- Whether or not the value needs quoting (has spaces/globs), or we don't know+data NumericalStatus = NumericalStatusUnknown | NumericalStatusEmpty | NumericalStatusMaybe | NumericalStatusDefinitely deriving (Show, Eq, Ord, Generic, NFData)++-- The set of possible sets of properties for this variable+type VariableProperties = S.Set (S.Set CFVariableProp)++defaultProperties = S.singleton S.empty++unknownVariableState = VariableState {+    variableValue = unknownVariableValue,+    variableProperties = defaultProperties+}++unknownVariableValue = VariableValue {+    literalValue = Nothing,+    spaceStatus = SpaceStatusDirty,+    numericalStatus = NumericalStatusUnknown+}++emptyVariableValue = unknownVariableValue {+    literalValue = Just "",+    spaceStatus = SpaceStatusEmpty,+    numericalStatus = NumericalStatusEmpty+}++unsetVariableState = VariableState {+    variableValue = emptyVariableValue,+    variableProperties = defaultProperties+}++mergeVariableState a b = VariableState {+    variableValue = mergeVariableValue (variableValue a) (variableValue b),+    variableProperties = S.union (variableProperties a) (variableProperties b)+}++mergeVariableValue a b = VariableValue {+    literalValue = if literalValue a == literalValue b then literalValue a else Nothing,+    spaceStatus = mergeSpaceStatus (spaceStatus a) (spaceStatus b),+    numericalStatus = mergeNumericalStatus (numericalStatus a) (numericalStatus b)+}++mergeSpaceStatus a b =+    case (a,b) of+        (SpaceStatusEmpty, y) -> y+        (x, SpaceStatusEmpty) -> x+        (SpaceStatusClean, SpaceStatusClean) -> SpaceStatusClean+        _ -> SpaceStatusDirty++mergeNumericalStatus a b =+    case (a,b) of+        (NumericalStatusDefinitely, NumericalStatusDefinitely) -> NumericalStatusDefinitely+        (NumericalStatusDefinitely, _) -> NumericalStatusMaybe+        (_, NumericalStatusDefinitely) -> NumericalStatusMaybe+        (NumericalStatusMaybe, _) -> NumericalStatusMaybe+        (_, NumericalStatusMaybe) -> NumericalStatusMaybe+        (NumericalStatusEmpty, NumericalStatusEmpty) -> NumericalStatusEmpty+        _ -> NumericalStatusUnknown++-- A VersionedMap is a Map that keeps an additional integer version to quickly determine if it has changed.+-- * Version -1 means it's unknown (possibly and presumably changed)+-- * Version 0 means it's empty+-- * Version N means it's equal to any other map with Version N (this is required but not enforced)+data VersionedMap k v = VersionedMap {+    mapVersion :: Integer,+    mapStorage :: M.Map k v+}+    deriving (Generic, NFData)++-- This makes states more readable but inhibits copy-paste+instance (Show k, Show v) => Show (VersionedMap k v) where+    show m = (if mapVersion m >= 0 then "V" ++ show (mapVersion m) else "U") ++ " " ++ show (mapStorage m)++instance Eq InternalState where+    (==) a b = stateIsQuickEqual a b || stateIsSlowEqual a b++instance (Eq k, Eq v) => Eq (VersionedMap k v) where+    (==) a b = vmIsQuickEqual a b || mapStorage a == mapStorage b++instance (Ord k, Ord v) => Ord (VersionedMap k v) where+    compare a b =+        if vmIsQuickEqual a b+        then EQ+        else mapStorage a `compare` mapStorage b+++-- A context with STRefs manually passed around to function.+-- This is done because it was dramatically much faster than any RWS type stack+data Ctx s = Ctx {+    -- The current node+    cNode :: STRef s Node,+    -- The current input state+    cInput :: STRef s InternalState,+    -- The current output state+    cOutput :: STRef s InternalState,++    -- The current functions/subshells stack+    cStack :: [StackEntry s],+    -- The input graph+    cGraph :: CFGraph,+    -- An incrementing counter to version maps+    cCounter :: STRef s Integer,+    -- A cache of input state dependencies to output effects+    cCache :: STRef s (M.Map Node [(S.Set StateDependency, InternalState)]),+    -- Whether the cache is enabled (see fallbackThreshold)+    cEnableCache :: STRef s Bool,+    -- The states resulting from data flows per invocation path+    cInvocations :: STRef s (M.Map [Node] (S.Set StateDependency, M.Map Node (InternalState, InternalState)))+}++-- Whenever a function (or subshell) is invoked, a value like this is pushed onto the stack+data StackEntry s = StackEntry {+    -- The entry point of this stack entry for the purpose of detecting recursion+    entryPoint :: Node,+    -- Whether this is a function call (as opposed to a subshell)+    isFunctionCall :: Bool,+    -- The node where this entry point was invoked+    callSite :: Node,+    -- A mutable set of dependencies we fetched from here or higher in the stack+    dependencies :: STRef s (S.Set StateDependency),+    -- The original input state for this stack entry+    stackState :: InternalState+}+    deriving (Eq, Generic, NFData)++#if MIN_VERSION_deepseq(1,4,2)+-- Our deepseq already has a STRef instance+#else+-- Older deepseq (for GHC < 8) lacks this instance+instance NFData (STRef s a) where+    rnf = (`seq` ())+#endif++-- Overwrite a base state with the contents of a diff state+-- This is unrelated to join/merge.+patchState :: InternalState -> InternalState -> InternalState+patchState base diff =+    case () of+        _ | sVersion diff == 0 -> base+        _ | sVersion base == 0 -> diff+        _ | stateIsQuickEqual base diff -> diff+        _ ->+            InternalState {+                sVersion = -1,+                sGlobalValues = vmPatch (sGlobalValues base) (sGlobalValues diff),+                sLocalValues = vmPatch (sLocalValues base) (sLocalValues diff),+                sPrefixValues = vmPatch (sPrefixValues base) (sPrefixValues diff),+                sFunctionTargets = vmPatch (sFunctionTargets base) (sFunctionTargets diff),+                sExitCodes = sExitCodes diff `mplus` sExitCodes base,+                sIsReachable = sIsReachable diff `mplus` sIsReachable base+            }++patchOutputM ctx diff = do+    let cOut = cOutput ctx+    oldState <- readSTRef cOut+    let newState = patchState oldState diff+    writeSTRef cOut newState++-- Merge (aka Join) two states. This is monadic because it requires looking up+-- values from the current context. For example:+--+--   f() {+--     foo || x=2+--     HERE         # This merge requires looking up the value of $x in the parent frame+--   }+--   x=1+--   f+mergeState :: forall s. Ctx s -> InternalState -> InternalState -> ST s InternalState+mergeState ctx a b = do+    -- Kludge: we want `readVariable` & friends not to read from an intermediate state,+    --         so temporarily set a blank input.+    let cin = cInput ctx+    old <- readSTRef cin+    writeSTRef cin newInternalState+    x <- merge a b+    writeSTRef cin old+    return x++  where++    merge a b =+        case () of+            _ | sIsReachable a == Just True && sIsReachable b == Just False+                    || sIsReachable a == Just False && sIsReachable b == Just True ->+                error $ pleaseReport "Unexpected merge of reachable and unreachable state"+            _ | sIsReachable a == Just False && sIsReachable b == Just False ->+                return unreachableState+            _ | sVersion a >= 0 && sVersion b >= 0 && sVersion a == sVersion b -> return a+            _ -> do+                globals <- mergeMaps ctx mergeVariableState readGlobal (sGlobalValues a) (sGlobalValues b)+                locals <- mergeMaps ctx mergeVariableState readVariable (sLocalValues a) (sLocalValues b)+                prefix <- mergeMaps ctx mergeVariableState readVariable (sPrefixValues a) (sPrefixValues b)+                funcs <- mergeMaps ctx S.union readFunction (sFunctionTargets a) (sFunctionTargets b)+                exitCodes <- mergeMaybes ctx S.union readExitCodes (sExitCodes a) (sExitCodes b)+                return $ InternalState {+                    sVersion = -1,+                    sGlobalValues = globals,+                    sLocalValues = locals,+                    sPrefixValues = prefix,+                    sFunctionTargets = funcs,+                    sExitCodes = exitCodes,+                    sIsReachable = liftM2 (&&) (sIsReachable a) (sIsReachable b)+                }++-- Merge a number of states, or return a default if there are no states+-- (it can't fold from newInternalState because this would be equivalent of adding a new input edge).+mergeStates :: forall s. Ctx s -> InternalState -> [InternalState] -> ST s InternalState+mergeStates ctx def list =+    case list of+        [] -> return def+        (first:rest) -> foldM (mergeState ctx) first rest++-- Merge two maps, key by key. If both maps have a key, the 'merger' is used.+-- If only one has the key, the 'reader' is used to fetch a second, and the two are merged as above.+mergeMaps :: (Ord k) => forall s.+    Ctx s ->+    (v -> v -> v) ->+    (Ctx s -> k -> ST s v) ->+    (VersionedMap k v) ->+    (VersionedMap k v) ->+    ST s (VersionedMap k v)+mergeMaps ctx merger reader a b =+    if vmIsQuickEqual a b+    then return a+    else do+        new <- M.fromDistinctAscList <$> reverse <$> f [] (M.toAscList $ mapStorage a) (M.toAscList $ mapStorage b)+        vmFromMap ctx new+  where+    f l [] [] = return l+    f l [] b = f l b []+    f l ((k,v):rest1) [] = do+        other <- reader ctx k+        f ((k, merger v other):l) rest1 []+    f l l1@((k1, v1):rest1) l2@((k2, v2):rest2) =+        case k1 `compare` k2 of+            EQ ->+                f ((k1, merger v1 v2):l) rest1 rest2+            LT -> do+                nv2 <- reader ctx k1+                f ((k1, merger v1 nv2):l) rest1 l2+            GT -> do+                nv1 <- reader ctx k2+                f ((k2, merger nv1 v2):l) l1 rest2++-- Merge two Maybes, like mergeMaps for a single element+mergeMaybes ctx merger reader a b =+    case (a, b) of+        (Nothing, Nothing) -> return Nothing+        (Just v1, Nothing) -> single v1+        (Nothing, Just v2) -> single v2+        (Just v1, Just v2) -> return $ Just $ merger v1 v2+  where+    single val = do+        result <- merger val <$> reader ctx+        return $ Just result++vmFromMap ctx map = return $ VersionedMap {+    mapVersion = -1,+    mapStorage = map+}++-- Give a VersionedMap a version if it does not already have one.+versionMap ctx map =+    if mapVersion map >= 0+    then return map+    else do+        v <- nextVersion ctx+        return map {+            mapVersion = v+        }++-- Give an InternalState a version if it does not already have one.+versionState ctx state =+    if sVersion state >= 0+    then return state+    else do+        self <- nextVersion ctx+        ssGlobalValues <- versionMap ctx $ sGlobalValues state+        ssLocalValues <- versionMap ctx $ sLocalValues state+        ssFunctionTargets <- versionMap ctx $ sFunctionTargets state+        return state {+            sVersion = self,+            sGlobalValues = ssGlobalValues,+            sLocalValues = ssLocalValues,+            sFunctionTargets = ssFunctionTargets+        }++-- Like 'not null' but for 2+ elements+is2plus :: [a] -> Bool+is2plus l = case l of+    _:_:_ -> True+    _ -> False++-- Use versions to see if two states are trivially identical+stateIsQuickEqual a b =+    let+        va = sVersion a+        vb = sVersion b+    in+        va >= 0 && vb >= 0 && va == vb++-- A manual slow path 'Eq' (it's not derived because it's part of the custom Eq instance)+stateIsSlowEqual a b =+    check sGlobalValues+    && check sLocalValues+    && check sPrefixValues+    && check sFunctionTargets+    && check sIsReachable+  where+    check f = f a == f b++-- Check if two VersionedMaps are trivially equal+vmIsQuickEqual :: VersionedMap k v -> VersionedMap k v -> Bool+vmIsQuickEqual a b =+    let+        va = mapVersion a+        vb = mapVersion b+    in+        va >= 0 && vb >= 0 && va == vb++-- A new, empty VersionedMap+vmEmpty = VersionedMap {+    mapVersion = 0,+    mapStorage = M.empty+}++-- Map.null for VersionedMaps+vmNull :: VersionedMap k v -> Bool+vmNull m = mapVersion m == 0 || (M.null $ mapStorage m)++-- Map.lookup for VersionedMaps+vmLookup name map = M.lookup name $ mapStorage map++-- Map.insert for VersionedMaps+vmInsert key val map = VersionedMap {+    mapVersion = -1,+    mapStorage = M.insert key val $ mapStorage map+}++-- Overwrite all keys in the first map with values from the second+vmPatch :: (Ord k) => VersionedMap k v -> VersionedMap k v -> VersionedMap k v+vmPatch base diff =+    case () of+        _ | mapVersion base == 0 -> diff+        _ | mapVersion diff == 0 -> base+        _ | vmIsQuickEqual base diff -> diff+        _ -> VersionedMap {+            mapVersion = -1,+            mapStorage = M.unionWith (flip const) (mapStorage base) (mapStorage diff)+        }++-- Set a variable. This includes properties. Applies it to the appropriate scope.+writeVariable :: forall s. Ctx s -> String -> VariableState -> ST s ()+writeVariable ctx name val = do+    typ <- readVariableScope ctx name+    case typ of+        GlobalScope -> writeGlobal ctx name val+        LocalScope -> writeLocal ctx name val+        -- Prefixed variables actually become local variables in the invoked function+        PrefixScope -> writeLocal ctx name val++writeGlobal ctx name val = do+    modifySTRef (cOutput ctx) $ insertGlobal name val++writeLocal ctx name val = do+    modifySTRef (cOutput ctx) $ insertLocal name val++writePrefix ctx name val = do+    modifySTRef (cOutput ctx) $ insertPrefix name val++updateVariableValue ctx name val = do+    (props, scope) <- readVariablePropertiesWithScope ctx name+    let f = case scope of+                GlobalScope -> writeGlobal+                LocalScope -> writeLocal+                PrefixScope -> writeLocal -- Updates become local+    f ctx name $ VariableState { variableValue = val, variableProperties = props }++updateGlobalValue ctx name val = do+    props <- readGlobalProperties ctx name+    writeGlobal ctx name VariableState { variableValue = val, variableProperties = props }++updateLocalValue ctx name val = do+    props <- readLocalProperties ctx name+    writeLocal ctx name VariableState { variableValue = val, variableProperties = props }++updatePrefixValue ctx name val = do+    -- Prefix variables don't inherit properties+    writePrefix ctx name VariableState { variableValue = val, variableProperties = defaultProperties }+++-- Look up a variable value, and also return its scope+readVariableWithScope :: forall s. Ctx s -> String -> ST s (VariableState, Scope)+readVariableWithScope ctx name = lookupStack get dep def ctx name+  where+    def = (unknownVariableState, GlobalScope)+    get = getVariableWithScope+    dep k (val, scope) = DepState scope k val++-- Look up the variable's properties. This can be done independently to avoid incurring a dependency on the value.+readVariablePropertiesWithScope :: forall s. Ctx s -> String -> ST s (VariableProperties, Scope)+readVariablePropertiesWithScope ctx name = lookupStack get dep def ctx name+  where+    def = (defaultProperties, GlobalScope)+    get s k = do+        (val, scope) <- getVariableWithScope s k+        return (variableProperties val, scope)+    dep k (val, scope) = DepProperties scope k val++readVariableScope ctx name = snd <$> readVariablePropertiesWithScope ctx name++getVariableWithScope :: InternalState -> String -> Maybe (VariableState, Scope)+getVariableWithScope s name =+    case (vmLookup name $ sPrefixValues s, vmLookup name $ sLocalValues s, vmLookup name $ sGlobalValues s) of+        (Just var, _, _) -> return (var, PrefixScope)+        (_, Just var, _) -> return (var, LocalScope)+        (_, _, Just var) -> return (var, GlobalScope)+        _ -> Nothing++undefineFunction ctx name =+    writeFunction ctx name $ FunctionUnknown++undefineVariable ctx name =+    writeVariable ctx name $ unsetVariableState++readVariable ctx name = fst <$> readVariableWithScope ctx name+readVariableProperties ctx name = fst <$> readVariablePropertiesWithScope ctx name++readGlobal ctx name = lookupStack get dep def ctx name+  where+    def = unknownVariableState -- could come from the environment+    get s name = vmLookup name $ sGlobalValues s+    dep k v = DepState GlobalScope k v+++readGlobalProperties ctx name = lookupStack get dep def ctx name+  where+    def = defaultProperties+    get s name = variableProperties <$> (vmLookup name $ sGlobalValues s)+    -- This dependency will fail to match if it's shadowed by a local variable,+    -- such as in  x=1; f() { local -i x; declare -ag x; } because we'll look at+    -- x and find it to be local and not global. FIXME?+    dep k v = DepProperties GlobalScope k v++readLocal ctx name = lookupStackUntilFunction get dep def ctx name+  where+    def = unsetVariableState -- can't come from the environment+    get s name = vmLookup name $ sLocalValues s+    dep k v = DepState LocalScope k v++-- We only want to look up the local properties of the current function,+-- though preferably even if we're in a subshell.  FIXME?+readLocalProperties ctx name = fst <$> lookupStackUntilFunction get dep def ctx name+  where+    def = (defaultProperties, LocalScope)+    with tag f = do+        val <- variableProperties <$> f+        return (val, tag)++    get s name = (with LocalScope $ vmLookup name $ sLocalValues s) `mplus` (with PrefixScope $ vmLookup name $ sPrefixValues s)+    dep k (val, scope) = DepProperties scope k val++readFunction ctx name = lookupStack get dep def ctx name+  where+    def = unknownFunctionValue+    get s name = vmLookup name $ sFunctionTargets s+    dep k v = DepFunction k v++writeFunction ctx name val = do+    modifySTRef (cOutput ctx) $ insertFunction name $ S.singleton val++readExitCodes ctx = lookupStack get dep def ctx ()+  where+    get s () = sExitCodes s+    def = S.empty+    dep () v = DepExitCodes v++-- Look up each state on the stack until a value is found (or the default is used),+-- then add this value as a StateDependency.+lookupStack' :: forall s k v.+    -- Whether to stop at function boundaries+    Bool+    -- A function that maybe finds a value from a state+    -> (InternalState -> k -> Maybe v)+    -- A function that creates a dependency on what was found+    -> (k -> v -> StateDependency)+    -- A default value, if the value can't be found anywhere+    -> v+    -- Context+    -> Ctx s+    -- The key to look up+    -> k+    -- Returning the result+    -> ST s v+lookupStack' functionOnly get dep def ctx key = do+    top <- readSTRef $ cInput ctx+    case get top key of+        Just v -> return v+        Nothing -> f (cStack ctx)+  where+    f [] = return def+    f (s:_) | functionOnly && isFunctionCall s = return def+    f (s:rest) = do+        -- Go up the stack until we find the value, and add+        -- a dependency on each state (including where it was found)+        res <- fromMaybe (f rest) (return <$> get (stackState s) key)+        modifySTRef (dependencies s) $ S.insert $ dep key res+        return res++lookupStack = lookupStack' False+lookupStackUntilFunction = lookupStack' True++-- Like lookupStack but without adding dependencies+peekStack get def ctx key = do+    top <- readSTRef $ cInput ctx+    case get top key of+        Just v -> return v+        Nothing -> f (cStack ctx)+  where+    f [] = return def+    f (s:rest) =+        case get (stackState s) key of+            Just v -> return v+            Nothing -> f rest++-- Check if the current context fulfills a StateDependency if entering `entry`+fulfillsDependency ctx entry dep =+    case dep of+        DepState scope name val -> (== (val, scope)) <$> peek scope ctx name+        DepProperties scope name props -> do+            (state, s) <- peek scope ctx name+            return $ scope == s && variableProperties state == props+        DepFunction name val -> (== val) <$> peekFunc ctx name+        -- Hack. Since we haven't pushed the soon-to-be invoked function on the stack,+        -- it won't be found by the normal check.+        DepIsRecursive node val | node == entry -> return True+        DepIsRecursive node val -> return $ val == any (\f -> entryPoint f == node) (cStack ctx)+        DepExitCodes val -> (== val) <$> peekStack (\s k -> sExitCodes s) S.empty ctx ()+  --      _ -> error $ "Unknown dep " ++ show dep+  where+    peek scope = peekStack getVariableWithScope $ if scope == GlobalScope then (unknownVariableState, GlobalScope) else (unsetVariableState, LocalScope)+    peekFunc = peekStack (\state name -> vmLookup name $ sFunctionTargets state) unknownFunctionValue++-- Check if the current context fulfills all StateDependencies+fulfillsDependencies ctx entry deps =+    f $ S.toList deps+  where+    f [] = return True+    f (dep:rest) = do+        res <- fulfillsDependency ctx entry dep+        if res+            then f rest+            else return False++-- Create a brand new Ctx given a Control Flow Graph (CFG)+newCtx g = do+    c <- newSTRef 1+    input <- newSTRef undefined+    output <- newSTRef undefined+    node <- newSTRef undefined+    cache <- newSTRef M.empty+    enableCache <- newSTRef True+    invocations <- newSTRef M.empty+    return $ Ctx {+        cCounter = c,+        cInput = input,+        cOutput = output,+        cNode = node,+        cCache = cache,+        cEnableCache = enableCache,+        cStack = [],+        cInvocations = invocations,+        cGraph = g+    }++-- The next incrementing version for VersionedMaps+nextVersion ctx = do+    let ctr = cCounter ctx+    n <- readSTRef ctr+    writeSTRef ctr $! n+1+    return n++-- Create a new StackEntry+newStackEntry ctx point isCall = do+    deps <- newSTRef S.empty+    state <- readSTRef $ cOutput ctx+    callsite <- readSTRef $ cNode ctx+    return $ StackEntry {+        entryPoint = point,+        isFunctionCall = isCall,+        callSite = callsite,+        dependencies = deps,+        stackState = state+    }++-- Call a function with a new stack entry on the stack+withNewStackFrame ctx node isCall f = do+    newEntry <- newStackEntry ctx node isCall+    newInput <- newSTRef newInternalState+    newOutput <- newSTRef newInternalState+    newNode <- newSTRef node+    let newCtx = ctx {+        cInput = newInput,+        cOutput = newOutput,+        cNode = newNode,+        cStack = newEntry : cStack ctx+    }+    x <- f newCtx++    {-+    deps <- readSTRef $ dependencies newEntry+    selfcheck <- fulfillsDependencies newCtx deps+    unless selfcheck $ error $ pleaseReport $ "Unmet stack dependencies on " ++ show (node, deps)+    -}++    return (x, newEntry)++-- Check if invoking this function would be a recursive loop+-- (i.e. we already have the function on the stack)+wouldBeRecursive ctx node = f (cStack ctx)+  where+    f [] = return False+    f (s:rest) = do+        res <-+            if entryPoint s == node+            then return True+            else f rest+        modifySTRef (dependencies s) $ S.insert $ DepIsRecursive node res+        return res++-- The main DFA 'transfer' function, applying the effects of a node to the output state+transfer ctx label =+  --traceShow ("Transferring", label) $+    case label of+        CFStructuralNode -> return ()+        CFEntryPoint _ -> return ()+        CFImpliedExit -> return ()+        CFResolvedExit {} -> return ()++        CFExecuteCommand cmd -> transferCommand ctx cmd+        CFExecuteSubshell reason entry exit -> transferSubshell ctx reason entry exit+        CFApplyEffects effects -> mapM_ (\(IdTagged _ f) -> transferEffect ctx f) effects+        CFSetExitCode id -> transferExitCode ctx id++        CFUnresolvedExit -> patchOutputM ctx unreachableState+        CFUnreachable -> patchOutputM ctx unreachableState++        -- TODO+        CFSetBackgroundPid _ -> return ()+        CFDropPrefixAssignments {} ->+            modifySTRef (cOutput ctx) $ \c -> modified c { sPrefixValues = vmEmpty }+--        _ -> error $ "Unknown " ++ show label+++-- Transfer the effects of a subshell invocation. This is similar to a function call+-- to allow easily discarding the effects (otherwise the InternalState would have+-- to represent subshell depth, while this way it can simply use the function stack).+transferSubshell ctx reason entry exit = do+    let cout = cOutput ctx+    initial <- readSTRef cout+    runCached ctx entry (f entry exit)+    res <- readSTRef cout+    -- Clear subshell changes. TODO: track this to warn about modifications.+    writeSTRef cout $ initial {+        sExitCodes = sExitCodes res+    }+  where+    f entry exit ctx = do+        (states, frame) <- withNewStackFrame ctx entry False (flip dataflow $ entry)+        let (_, res) = fromMaybe (error $ pleaseReport "Subshell has no exit") $ M.lookup exit states+        deps <- readSTRef $ dependencies frame+        registerFlowResult ctx entry states deps+        return (deps, res)++-- Transfer the effects of executing a command, i.e. the merged union of all possible function definitions.+transferCommand ctx Nothing = return ()+transferCommand ctx (Just name) = do+    targets <- readFunction ctx name+    logVerbose ("Transferring ",name,targets)+    transferMultiple ctx $ map (flip transferFunctionValue) $ S.toList targets++-- Transfer a set of function definitions and merge the output states.+transferMultiple ctx funcs = do+    logVerbose ("Transferring set of ", length funcs)+    original <- readSTRef out+    branches <- mapM (apply ctx original) funcs+    merged <- mergeStates ctx original branches+    let patched = patchState original merged+    writeSTRef out patched+  where+    out = cOutput ctx+    apply ctx original f = do+        writeSTRef out original+        f ctx+        readSTRef out++-- Transfer the effects of a single function definition.+transferFunctionValue ctx funcVal =+    case funcVal of+        FunctionUnknown -> return ()+        FunctionDefinition name entry exit -> do+            isRecursive <- wouldBeRecursive ctx entry+            if isRecursive+                then return () -- TODO: Find a better strategy for recursion+                else runCached ctx entry (f name entry exit)+  where+    f name entry exit ctx = do+        (states, frame) <- withNewStackFrame ctx entry True (flip dataflow $ entry)+        deps <- readSTRef $ dependencies frame+        let res =+                case M.lookup exit states of+                    Just (input, output) -> do+                        -- Discard local variables. TODO: track&retain variables declared local in previous scopes?+                        modified output { sLocalValues = vmEmpty }+                    Nothing -> do+                        -- e.g. f() { exit; }+                        unreachableState+        registerFlowResult ctx entry states deps+        return (deps, res)++transferExitCode ctx id = do+    modifySTRef (cOutput ctx) $ setExitCode id++-- Register/save the result of a dataflow of a function.+-- At the end, all the different values from different flows are merged together.+registerFlowResult ctx entry states deps = do+    -- This function is called in the context of a CFExecuteCommand and not its invoked function,+    -- so manually add the current node to the stack.+    current <- readSTRef $ cNode ctx+    let parents = map callSite $ cStack ctx+    -- A unique path to this flow context. The specific value doesn't matter, as long as it's+    -- unique per invocation of the function. This is required so that 'x=1; f; x=2; f' won't+    -- overwrite each other.+    let path = entry : current : parents+    modifySTRef (cInvocations ctx) $ M.insert path (deps, states)+++-- Look up a node in the cache and see if the dependencies of any entries are matched.+-- In that case, reuse the previous result instead of doing a new data flow.+runCached :: forall s. Ctx s -> Node -> (Ctx s -> ST s (S.Set StateDependency, InternalState)) -> ST s ()+runCached ctx node f = do+    cache <- getCache ctx node+    case cache of+        Just v -> do+            logInfo ("Running cached", node)+            -- do { (deps, diff) <- f ctx; unless (v == diff) $ traceShowM ("Cache FAILED to match actual result", node, deps, diff); }+            patchOutputM ctx v++        Nothing -> do+            logInfo ("Cache failed", node)+            (deps, diff) <- f ctx+            modifySTRef (cCache ctx) (M.insertWith (\_ old -> (deps, diff):(take cacheEntries old)) node [(deps,diff)])+            logVerbose ("Recomputed cache for", node, deps)+            -- do { f <- fulfillsDependencies ctx node deps; unless (f) $ traceShowM ("New dependencies FAILED to match", node, deps); }+            patchOutputM ctx diff++-- Get a cached version whose dependencies are currently fulfilled, if any.+getCache :: forall s. Ctx s -> Node -> ST s (Maybe InternalState)+getCache ctx node = do+    cache <- readSTRef $ cCache ctx+    enable <- readSTRef $ cEnableCache ctx+    logVerbose ("Cache for", node, "length", length $ M.findWithDefault [] node cache, M.lookup node cache)+    if enable+        then f $ M.findWithDefault [] node cache+        else return Nothing+  where+    f [] = return Nothing+    f ((deps, value):rest) = do+        match <- fulfillsDependencies ctx node deps+        if match+            then return $ Just value+            else f rest++-- Transfer a single CFEffect to the output state.+transferEffect ctx effect =+    case effect of+        CFReadVariable name ->+            case name of+                "?" -> void $ readExitCodes ctx+                _ -> void $ readVariable ctx name+        CFWriteVariable name value -> do+            val <- cfValueToVariableValue ctx value+            updateVariableValue ctx name val+        CFWriteGlobal name value -> do+            val <- cfValueToVariableValue ctx value+            updateGlobalValue ctx name val+        CFWriteLocal name value -> do+            val <- cfValueToVariableValue ctx value+            updateLocalValue ctx name val+        CFWritePrefix name value -> do+            val <- cfValueToVariableValue ctx value+            updatePrefixValue ctx name val++        CFSetProps scope name props ->+            case scope of+                DefaultScope -> do+                    state <- readVariable ctx name+                    writeVariable ctx name $ addProperties props state+                GlobalScope -> do+                    state <- readGlobal ctx name+                    writeGlobal ctx name $ addProperties props state+                LocalScope -> do+                    out <- readSTRef (cOutput ctx)+                    state <- readLocal ctx name+                    writeLocal ctx name $ addProperties props state+                PrefixScope -> do+                    -- Prefix values become local+                    state <- readLocal ctx name+                    writeLocal ctx name $ addProperties props state++        CFUnsetProps scope name props ->+            case scope of+                DefaultScope -> do+                    state <- readVariable ctx name+                    writeVariable ctx name $ removeProperties props state+                GlobalScope -> do+                    state <- readGlobal ctx name+                    writeGlobal ctx name $ removeProperties props state+                LocalScope -> do+                    out <- readSTRef (cOutput ctx)+                    state <- readLocal ctx name+                    writeLocal ctx name $ removeProperties props state+                PrefixScope -> do+                    -- Prefix values become local+                    state <- readLocal ctx name+                    writeLocal ctx name $ removeProperties props state+++        CFUndefineVariable name -> undefineVariable ctx name+        CFUndefineFunction name -> undefineFunction ctx name+        CFUndefine name -> do+            -- This should really just unset one or the other+            undefineVariable ctx name+            undefineFunction ctx name+        CFDefineFunction name id entry exit ->+            writeFunction ctx name $ FunctionDefinition name entry exit++        -- TODO+        CFUndefineNameref name -> undefineVariable ctx name+        CFHintArray name -> return ()+        CFHintDefined name -> return ()+--        _ -> error $ "Unknown effect " ++ show effect+++-- Transfer the CFG's idea of a value into our VariableState+cfValueToVariableValue ctx val =+    case val of+        CFValueArray -> return unknownVariableValue -- TODO: Track array status+        CFValueComputed _ parts -> foldM f emptyVariableValue parts+        CFValueInteger -> return unknownIntegerValue+        CFValueString -> return unknownVariableValue+        CFValueUninitialized -> return emptyVariableValue+--        _ -> error $ "Unknown value: " ++ show val+  where+    f val part = do+        next <- computeValue ctx part+        return $ val `appendVariableValue` next++-- A value can be computed from 0 or more parts, such as x="literal$y$z"+computeValue ctx part =+    case part of+        CFStringLiteral str -> return $ literalToVariableValue str+        CFStringInteger -> return unknownIntegerValue+        CFStringUnknown -> return unknownVariableValue+        CFStringVariable name -> variableStateToValue <$> readVariable ctx name+   where+    variableStateToValue state =+        case () of+            _ | all (CFVPInteger `S.member`) $ variableProperties state -> unknownIntegerValue+            _ -> variableValue state++-- Append two VariableValues as if with z="$x$y"+appendVariableValue :: VariableValue -> VariableValue -> VariableValue+appendVariableValue a b =+    unknownVariableValue {+        literalValue = liftM2 (++) (literalValue a) (literalValue b),+        spaceStatus = appendSpaceStatus (spaceStatus a) (spaceStatus b),+        numericalStatus = appendNumericalStatus (numericalStatus a) (numericalStatus b)+    }++appendSpaceStatus a b =+    case (a,b) of+        (SpaceStatusEmpty, _) -> b+        (_, SpaceStatusEmpty) -> a+        (SpaceStatusClean, SpaceStatusClean) -> a+        _ ->SpaceStatusDirty++appendNumericalStatus a b =+    case (a,b) of+        (NumericalStatusEmpty, x) -> x+        (x, NumericalStatusEmpty) -> x+        (NumericalStatusDefinitely, NumericalStatusDefinitely) -> NumericalStatusDefinitely+        (NumericalStatusUnknown, _) -> NumericalStatusUnknown+        (_, NumericalStatusUnknown) -> NumericalStatusUnknown+        _ -> NumericalStatusMaybe++unknownIntegerValue = unknownVariableValue {+    literalValue = Nothing,+    spaceStatus = SpaceStatusClean,+    numericalStatus = NumericalStatusDefinitely+}++literalToVariableValue str = unknownVariableValue {+    literalValue = Just str,+    spaceStatus = literalToSpaceStatus str,+    numericalStatus = literalToNumericalStatus str+}++withoutChanges ctx f = do+    let inp = cInput ctx+    let out = cOutput ctx+    prevInput <- readSTRef inp+    prevOutput <- readSTRef out+    res <- f+    writeSTRef inp prevInput+    writeSTRef out prevOutput+    return res++-- Get the SpaceStatus for a literal string, i.e. if it needs quoting+literalToSpaceStatus str =+    case str of+        "" -> SpaceStatusEmpty+        _ | all (`notElem` " \t\n*?[") str -> SpaceStatusClean+        _ -> SpaceStatusDirty++-- Get the NumericalStatus for a literal string, i.e. whether it's an integer+literalToNumericalStatus str =+    case str of+        "" -> NumericalStatusEmpty+        '-':rest -> if isNumeric rest then NumericalStatusDefinitely else NumericalStatusUnknown+        rest -> if isNumeric rest then NumericalStatusDefinitely else NumericalStatusUnknown+  where+    isNumeric = all isDigit++type StateMap = M.Map Node (InternalState, InternalState)++-- Classic, iterative Data Flow Analysis. See Wikipedia for a description of the process.+dataflow :: forall s. Ctx s -> Node -> ST s StateMap+dataflow ctx entry = do+    pending <- newSTRef $ S.singleton entry+    states <- newSTRef $ M.empty+    -- Should probably be done via a stack frame instead+    withoutChanges ctx $+        f iterationCount pending states+    readSTRef states+  where+    graph = cGraph ctx+    f 0 _ _ = error $ pleaseReport "DFA did not reach fix point"+    f n pending states = do+        ps <- readSTRef pending++        when (n == fallbackThreshold) $ do+            -- This should never happen, but has historically been due to caching bugs.+            -- Try disabling the cache and continuing.+            logInfo "DFA is not stabilizing! Disabling cache."+            writeSTRef (cEnableCache ctx) False++        if S.null ps+            then return ()+            else do+                let (next, rest) = S.deleteFindMin ps+                nexts <- process states next+                writeSTRef pending $ foldl (flip S.insert) rest nexts+                f (n-1) pending states++    process states node = do+        stateMap <- readSTRef states+        let inputs = filter (\c -> sIsReachable c /= Just False) $ mapMaybe (\c -> fmap snd $ M.lookup c stateMap) incoming+        input <-+            case incoming of+                [] -> return newInternalState+                _ ->+                    case inputs of+                        [] -> return unreachableState+                        (x:rest) -> foldM (mergeState ctx) x rest+        writeSTRef (cInput ctx) $ input+        writeSTRef (cOutput ctx) $ input+        writeSTRef (cNode ctx) $ node+        transfer ctx label+        newOutput <- readSTRef $ cOutput ctx+        result <-+            if is2plus outgoing+            then+                -- Version the state because we split and will probably merge later+                versionState ctx newOutput+            else return newOutput+        writeSTRef states $ M.insert node (input, result) stateMap+        case M.lookup node stateMap of+            Nothing -> return outgoing+            Just (oldInput, oldOutput) ->+                if oldOutput == result+                then return []+                else return outgoing+      where+        (incomingL, _, label, outgoingL) = context graph $ node+        incoming = map snd $ filter isRegular $ incomingL+        outgoing = map snd outgoingL+        isRegular = ((== CFEFlow) . fst)++runRoot ctx env entry exit = do+    writeSTRef (cInput ctx) $ env+    writeSTRef (cOutput ctx) $ env+    writeSTRef (cNode ctx) $ entry+    (states, frame) <- withNewStackFrame ctx entry False $ \c -> dataflow c entry+    deps <- readSTRef $ dependencies frame+    registerFlowResult ctx entry states deps+    -- Return the final state, used to invoke functions that were declared but not invoked+    return $ snd $ fromMaybe (error $ pleaseReport "Missing exit state") $ M.lookup exit states+++analyzeControlFlow :: CFGParameters -> Token -> CFGAnalysis+analyzeControlFlow params t =+    let+        cfg = buildGraph params t+        (entry, exit) = M.findWithDefault (error $ pleaseReport "Missing root") (getId t) (cfIdToRange cfg)+    in+        runST $ f cfg entry exit+  where+    f cfg entry exit = do+        let env = createEnvironmentState+        ctx <- newCtx $ cfGraph cfg+        -- Do a dataflow analysis starting on the root node+        exitState <- runRoot ctx env entry exit++        -- All nodes we've touched+        invocations <- readSTRef $ cInvocations ctx+        let invokedNodes = M.fromDistinctAscList $ map (\c -> (c, ())) $ S.toList $ M.keysSet $ groupByNode $ M.map snd invocations++        -- Invoke all functions that were declared but not invoked+        -- This is so that we still get warnings for dead code+        -- (it's probably not actually dead, just used by a script that sources ours)+        let declaredFunctions = getFunctionTargets exitState+        let uninvoked = M.difference declaredFunctions invokedNodes++        let stragglerInput =+                (env `patchState` exitState) {+                    -- We don't want `die() { exit $?; }; echo "Sourced"` to assume $? is always echo+                    sExitCodes = Nothing+                }++        analyzeStragglers ctx stragglerInput uninvoked++        -- Now round up all the states from all data flows+        -- (FIXME: this excludes functions that were defined in straggling functions)+        invocations <- readSTRef $ cInvocations ctx+        invokedStates <- flattenByNode ctx $ groupByNode $ M.map addDeps invocations++        -- Fill in the map with unreachable states for anything we didn't get to+        let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg+        let allStates = M.unionWith (flip const) baseStates invokedStates++        -- Convert to external states+        let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates++        return $ nodeToData `deepseq` CFGAnalysis {+            graph = cfGraph cfg,+            tokenToRange = cfIdToRange cfg,+            tokenToNodes = cfIdToNodes cfg,+            nodeToData = nodeToData,+            postDominators = cfPostDominators cfg+        }+++    -- Include the dependencies in the state of each function, e.g. if it depends on `x=foo` then add that.+    addDeps :: (S.Set StateDependency, M.Map Node (InternalState, InternalState)) -> M.Map Node (InternalState, InternalState)+    addDeps (deps, m) = let base = depsToState deps in M.map (\(a,b) -> (base `patchState` a, base `patchState` b)) m++    -- Collect all the states that each node has resulted in.+    groupByNode :: forall k v. M.Map k (M.Map Node v) -> M.Map Node [v]+    groupByNode pathMap = M.fromListWith (++) $ map (\(k,v) -> (k,[v])) $ concatMap M.toList $ M.elems pathMap++    -- Merge all the pre/post states for each node. This would have been a foldM if Map had one.+    flattenByNode ctx m = M.fromDistinctAscList <$> (mapM (mergePair ctx) $ M.toList m)++    mergeAllStates ctx pairs =+        let+            (pres, posts) = unzip pairs+        in do+            pre <- mergeStates ctx (error $ pleaseReport "Null node states") pres+            post <- mergeStates ctx (error $ pleaseReport "Null node states") posts+            return (pre, post)++    mergePair ctx (node, list) = do+        merged <- mergeAllStates ctx list+        return (node, merged)++    -- Get the all the functions defined in an InternalState+    getFunctionTargets :: InternalState -> M.Map Node FunctionDefinition+    getFunctionTargets state =+        let+            declaredFuncs = S.unions $ M.elems $ mapStorage $ sFunctionTargets state+            getFunc d =+                case d of+                    FunctionDefinition _ entry _ -> Just (entry, d)+                    _ -> Nothing+            funcs = mapMaybe getFunc $ S.toList declaredFuncs+        in+            M.fromList funcs+++analyzeStragglers ctx state stragglers = do+    mapM_ f $ M.elems stragglers+  where+    f def@(FunctionDefinition name entry exit) = do+        writeSTRef (cInput ctx) state+        writeSTRef (cOutput ctx) state+        writeSTRef (cNode ctx) entry+        transferFunctionValue ctx def++++return []+runTests = $quickCheckAll
src/ShellCheck/Checker.hs view
@@ -1,5 +1,5 @@ {--    Copyright 2012-2020 Vidar Holen+    Copyright 2012-2022 Vidar Holen      This file is part of ShellCheck.     https://www.shellcheck.net@@ -20,9 +20,10 @@ {-# LANGUAGE TemplateHaskell #-} module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where +import ShellCheck.Analyzer+import ShellCheck.ASTLib import ShellCheck.Interface import ShellCheck.Parser-import ShellCheck.Analyzer  import Data.Either import Data.Functor@@ -85,7 +86,7 @@                     asCheckSourced = csCheckSourced spec,                     asExecutionMode = Executed,                     asTokenPositions = tokenPositions,-                    asOptionalChecks = csOptionalChecks spec+                    asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec                 } where as = newAnalysisSpec root         let analysisMessages =                 maybe []@@ -243,6 +244,9 @@ prop_canSourceDynamicWhenRedirected =     null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" +prop_canRedirectWithSpaces =+    null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""+ prop_recursiveAnalysis =     [2086] == checkRecursive [("lib", "echo $1")] "source lib" @@ -412,6 +416,15 @@         csCheckSourced = True     } +prop_sourcePathWorksWithSpaces = result == [2086]+  where+    f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"+    result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {+        csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",+        csFilename = "dir/myscript",+        csCheckSourced = True+    }+ prop_sourcePathRedirectsDirective = result == [2086]   where     f "dir/myscript" _ _ "lib" = return "foo/lib"@@ -483,6 +496,17 @@         csCheckSourced = True     } +prop_rcCanSuppressEarlyProblems1 = null result+  where+    result = checkWithRc "disable=1071" emptyCheckSpec {+        csScript = "#!/bin/zsh\necho $1"+    }++prop_rcCanSuppressEarlyProblems2 = null result+  where+    result = checkWithRc "disable=1104" emptyCheckSpec {+        csScript = "!/bin/bash\necho 'hello world'"+    }  return [] runTests = $quickCheckAll
src/ShellCheck/Checks/Commands.hs view
@@ -1,5 +1,5 @@ {--    Copyright 2012-2021 Vidar Holen+    Copyright 2012-2022 Vidar Holen      This file is part of ShellCheck.     https://www.shellcheck.net@@ -27,21 +27,28 @@ import ShellCheck.AST import ShellCheck.ASTLib import ShellCheck.AnalyzerLib+import ShellCheck.CFG+import qualified ShellCheck.CFGAnalysis as CF import ShellCheck.Data import ShellCheck.Interface import ShellCheck.Parser+import ShellCheck.Prelude import ShellCheck.Regex  import Control.Monad import Control.Monad.RWS import Data.Char import Data.Functor.Identity+import qualified Data.Graph.Inductive.Graph as G import Data.List import Data.Maybe-import qualified Data.Map.Strict as Map+import qualified Data.Map.Strict as M+import qualified Data.Set as S import Test.QuickCheck.All (forAllProperties) import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess) +import Debug.Trace -- STRIP+ data CommandName = Exactly String | Basename String     deriving (Eq, Ord) @@ -98,8 +105,10 @@     ,checkUnquotedEchoSpaces     ,checkEvalArray     ]-    ++ map checkArgComparison declaringCommands+    ++ map checkArgComparison ("alias" : declaringCommands)     ++ map checkMaskedReturns declaringCommands+    ++ map checkMultipleDeclaring declaringCommands+    ++ map checkBackreferencingDeclaration declaringCommands   optionalChecks = map fst optionalCommandChecks@@ -112,7 +121,7 @@         cdNegative = "command -v javac"     }, checkWhich)     ]-optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks+optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks  prop_verifyOptionalExamples = all check optionalCommandChecks   where@@ -161,27 +170,27 @@ prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts  -buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)-buildCommandMap = foldl' addCheck Map.empty+buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)+buildCommandMap = foldl' addCheck M.empty   where     addCheck map (CommandCheck name function) =-        Map.insertWith composeAnalyzers name function map+        M.insertWith composeAnalyzers name function map  -checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis+checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do     name <- getLiteralString cmd     return $         if '/' `elem` name         then-            Map.findWithDefault nullCheck (Basename $ basename name) map t+            M.findWithDefault nullCheck (Basename $ basename name) map t         else if name == "builtin" && not (null rest) then             let t' = T_SimpleCommand id cmdPrefix rest                 selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest-            in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'+            in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'         else do-            Map.findWithDefault nullCheck (Exactly name) map t-            Map.findWithDefault nullCheck (Basename name) map t+            M.findWithDefault nullCheck (Exactly name) map t+            M.findWithDefault nullCheck (Basename name) map t    where     basename = reverse . takeWhile (/= '/') . reverse@@ -203,22 +212,22 @@     optionals =         if "all" `elem` keys         then map snd optionalCommandChecks-        else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys+        else mapMaybe (\x -> M.lookup x optionalCheckMap) keys  prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"-prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"+prop_checkTr2a = verify checkTr "tr '[a-z]' '[A-Z]'" prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"-prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"-prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"+prop_checkTr3a = verifyNot checkTr "tr -d '[:upper:]'"+prop_checkTr3b = verifyNot checkTr "tr -d '|/_[:upper:]'" prop_checkTr4 = verifyNot checkTr "ls [a-z]" prop_checkTr5 = verify checkTr "tr foo bar" prop_checkTr6 = verify checkTr "tr 'hello' 'world'" prop_checkTr8 = verifyNot checkTr "tr aeiou _____" prop_checkTr9 = verifyNot checkTr "a-z n-za-m"-prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"-prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"-prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"+prop_checkTr10 = verifyNot checkTr "tr --squeeze-repeats rl lr"+prop_checkTr11 = verifyNot checkTr "tr abc '[d*]'"+prop_checkTr12 = verifyNot checkTr "tr '[=e=]' 'e'" checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)   where     f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?@@ -330,20 +339,20 @@ prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file" prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"-prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"-prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"-prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"-prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"-prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"-prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"-prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file"-prop_checkGrepRe17= verifyNot checkGrepRe "grep --exclude 'Foo*' file"-prop_checkGrepRe18= verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"-prop_checkGrepRe19= verify checkGrepRe "grep -- 'Foo*' file"-prop_checkGrepRe20= verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"-prop_checkGrepRe21= verifyNot checkGrepRe "grep -o 'x*' file"-prop_checkGrepRe22= verifyNot checkGrepRe "grep --only-matching 'x*' file"-prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"+prop_checkGrepRe10 = verifyNot checkGrepRe "grep '^aa*' file"+prop_checkGrepRe11 = verifyNot checkGrepRe "grep --include=*.png foo"+prop_checkGrepRe12 = verifyNot checkGrepRe "grep -F 'Foo*' file"+prop_checkGrepRe13 = verifyNot checkGrepRe "grep -- -foo bar*"+prop_checkGrepRe14 = verifyNot checkGrepRe "grep -e -foo bar*"+prop_checkGrepRe15 = verifyNot checkGrepRe "grep --regex -foo bar*"+prop_checkGrepRe16 = verifyNot checkGrepRe "grep --include 'Foo*' file"+prop_checkGrepRe17 = verifyNot checkGrepRe "grep --exclude 'Foo*' file"+prop_checkGrepRe18 = verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"+prop_checkGrepRe19 = verify checkGrepRe "grep -- 'Foo*' file"+prop_checkGrepRe20 = verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"+prop_checkGrepRe21 = verifyNot checkGrepRe "grep -o 'x*' file"+prop_checkGrepRe22 = verifyNot checkGrepRe "grep --only-matching 'x*' file"+prop_checkGrepRe23 = verifyNot checkGrepRe "grep '.*' file"  checkGrepRe = CommandCheck (Basename "grep") check where     check cmd = f cmd (arguments cmd)@@ -391,7 +400,7 @@   prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"-prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"+prop_checkTrapQuotes1a = verify checkTrapQuotes "trap \"echo `ls`\" INT" prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT" prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG" checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where@@ -472,9 +481,16 @@ prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"+prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"+prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"+prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"+prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"+prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"+prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"+prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'" checkUnusedEchoEscapes = CommandCheck (Basename "echo") f   where-    hasEscapes = mkRegex "\\\\[rnt]"+    hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"     f cmd =         whenShell [Sh, Bash, Ksh] $             unless (cmd `hasFlag` "e") $@@ -648,19 +664,19 @@ prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz" prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"-prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"-prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"-prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"-prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"-prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"-prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"-prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"-prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"-prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"-prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"-prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"-prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"-prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"+prop_checkPrintfVar10 = verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"+prop_checkPrintfVar11 = verifyNot checkPrintfVar "printf '%(%s%s)T' -1"+prop_checkPrintfVar12 = verify checkPrintfVar "printf '%s %s\\n' 1 2 3"+prop_checkPrintfVar13 = verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"+prop_checkPrintfVar14 = verify checkPrintfVar "printf '%*s\\n' 1"+prop_checkPrintfVar15 = verifyNot checkPrintfVar "printf '%*s\\n' 1 2"+prop_checkPrintfVar16 = verifyNot checkPrintfVar "printf $'string'"+prop_checkPrintfVar17 = verify checkPrintfVar "printf '%-*s\\n' 1"+prop_checkPrintfVar18 = verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"+prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'"+prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42"+prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'"+prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo"  checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where     f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest@@ -674,6 +690,7 @@             let formats = getPrintfFormats string             let formatCount = length formats             let argCount = length more+            let pluraliseIfMany word n = if n > 1 then word ++ "s" else word              return $ if                 | argCount == 0 && formatCount == 0 ->@@ -689,7 +706,8 @@                     return () -- Great: a suitable number of arguments                 | otherwise ->                     warn (getId format) 2183 $-                        "This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."+                        "This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount +++                        ", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."          unless ('%' `elem` concat (oversimplify format) || isLiteral format) $           info (getId format) 2059@@ -735,7 +753,7 @@         --            \____ _____/\___ ____/   \____ ____/\_________ _________/ \______ /         --                 V          V             V               V               V         --               flags    field width  precision   format character        rest-        -- field width and precision can be specified with a '*' instead of a digit,+        -- field width and precision can be specified with an '*' instead of a digit,         -- in which case printf will accept one more argument for each '*' used  @@ -941,6 +959,22 @@         unless (any isFunctionLike path) $             err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." +prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"+prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"+prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"+prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"+prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"+prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"+prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"+checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)+  where+    check t = sequence_ $ do+        lit <- getUnquotedLiteral t+        guard $ lit `elem` declaringCommands+        return $ err (getId $ getCommandTokenOrThis t) 2316 $+                 "This applies " ++ cmd ++ " to the variable named " ++ lit +++                 ", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."+ prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $@@ -987,20 +1021,20 @@      check :: Id -> [String] -> Token -> Analysis     check optId opts (T_CaseExpression id _ list) = do-            unless (Nothing `Map.member` handledMap) $ do-                mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled+            unless (Nothing `M.member` handledMap) $ do+                mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled -                unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $+                unless (any (`M.member` handledMap) [Just "*",Just "?"]) $                     warn id 2220 "Invalid flags are not handled. Add a *) case." -            mapM_ warnRedundant $ Map.toList notRequested+            mapM_ warnRedundant $ M.toList notRequested          where-            handledMap = Map.fromList (concatMap getHandledStrings list)-            requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts+            handledMap = M.fromList (concatMap getHandledStrings list)+            requestedMap = M.fromList $ map (\x -> (Just x, ())) opts -            notHandled = Map.difference requestedMap handledMap-            notRequested = Map.difference handledMap requestedMap+            notHandled = M.difference requestedMap handledMap+            notRequested = M.difference handledMap requestedMap      warnUnhandled optId caseId str =         warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."@@ -1044,10 +1078,10 @@ prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"-prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"-prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"-prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"-prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"+prop_checkCatastrophicRm10 = verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"+prop_checkCatastrophicRm11 = verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"+prop_checkCatastrophicRm12 = verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"+prop_checkCatastrophicRm13 = verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->@@ -1253,6 +1287,7 @@ prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b" prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo" prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"+prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0" -- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual   where@@ -1353,10 +1388,10 @@         m <- asks tokenPositions         redir <- getClosestCommandM t         sequence_ $ do-            let positions = mapMaybe (\c -> Map.lookup (getId c) m) args+            let positions = mapMaybe (\c -> M.lookup (getId c) m) args             let pairs = zip positions (drop 1 positions)             (T_Redirecting _ redirTokens _) <- redir-            let redirPositions = mapMaybe (\c -> fst <$> Map.lookup (getId c) m) redirTokens+            let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens             guard $ any (hasSpacesBetween redirPositions) pairs             return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one." @@ -1384,6 +1419,52 @@             -- Match ${arr[@]@Q} and ${@@Q} and such             T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l)             _ -> False+++prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"+prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"+prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"+prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"+prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"+prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"+prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"+checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check+  where+    check t = foldM_ perArg M.empty $ arguments t++    perArg leftArgs t =+        case t of+            T_Assignment id _ name idx t -> do+                warnIfBackreferencing leftArgs $ t:idx+                return $ M.insert name id leftArgs+            t -> do+                warnIfBackreferencing leftArgs [t]+                return leftArgs++    warnIfBackreferencing backrefs l = do+        references <- findReferences l+        let reused = M.intersection backrefs references+        mapM msg $ M.toList reused++    msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."++    findReferences list = do+        cfga <- asks cfgAnalysis+        let graph = CF.graph cfga+        let nodesMap = CF.tokenToNodes cfga+        let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list+        let labels = mapMaybe (G.lab graph) $ S.toList nodes+        let references = M.fromList $ concatMap refFromLabel labels+        return references++    refFromLabel lab =+        case lab of+            CFApplyEffects effects -> mapMaybe refFromEffect effects+            _ -> []+    refFromEffect e =+        case e of+            IdTagged id (CFReadVariable name) -> return (name, id)+            _ -> Nothing   return []
+ src/ShellCheck/Checks/ControlFlow.hs view
@@ -0,0 +1,101 @@+{-+    Copyright 2022 Vidar Holen++    This file is part of ShellCheck.+    https://www.shellcheck.net++    ShellCheck is free software: you can redistribute it and/or modify+    it under the terms of the GNU General Public License as published by+    the Free Software Foundation, either version 3 of the License, or+    (at your option) any later version.++    ShellCheck is distributed in the hope that it will be useful,+    but WITHOUT ANY WARRANTY; without even the implied warranty of+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+    GNU General Public License for more details.++    You should have received a copy of the GNU General Public License+    along with this program.  If not, see <https://www.gnu.org/licenses/>.+-}+{-# LANGUAGE TemplateHaskell #-}++-- Checks that run on the Control Flow Graph (as opposed to the AST)+-- This is scaffolding for a work in progress.++module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where++import ShellCheck.AST+import ShellCheck.ASTLib+import ShellCheck.CFG hiding (cfgAnalysis)+import ShellCheck.CFGAnalysis+import ShellCheck.AnalyzerLib+import ShellCheck.Data+import ShellCheck.Interface++import Control.Monad+import Control.Monad.Reader+import Data.Graph.Inductive.Graph+import qualified Data.Map as M+import qualified Data.Set as S+import Data.List+import Data.Maybe++import Test.QuickCheck.All (forAllProperties)+import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)+++optionalChecks :: [CheckDescription]+optionalChecks = []++-- A check that runs on the entire graph+type ControlFlowCheck = Analysis+-- A check invoked once per node, with its (pre,post) data+type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis+-- A check invoked once per effect, with its node's (pre,post) data+type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis+++checker :: AnalysisSpec -> Parameters -> Checker+checker spec params = Checker {+    perScript = const $ sequence_ controlFlowChecks,+    perToken = const $ return ()+}++controlFlowChecks :: [ControlFlowCheck]+controlFlowChecks = [+        runNodeChecks controlFlowNodeChecks+    ]++controlFlowNodeChecks :: [ControlFlowNodeCheck]+controlFlowNodeChecks = [+    runEffectChecks controlFlowEffectChecks+    ]++controlFlowEffectChecks :: [ControlFlowEffectCheck]+controlFlowEffectChecks = [+    ]++runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck+runNodeChecks perNode = do+    cfg <- asks cfgAnalysis+    runOnAll cfg+  where+    getData datas n@(node, label) = do+        (pre, post) <- M.lookup node datas+        return (n, (pre, post))++    runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis+    runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode+    runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)++runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck+runEffectChecks list = checkNode+  where+    checkNode (node, label) prepost =+        case label of+            CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects+            _ -> return ()+++return []+runTests =  $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
src/ShellCheck/Checks/ShellSupport.hs view
@@ -25,6 +25,7 @@ import ShellCheck.ASTLib import ShellCheck.AnalyzerLib import ShellCheck.Interface+import ShellCheck.Prelude import ShellCheck.Regex  import Control.Monad@@ -91,55 +92,57 @@ prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"-prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"-prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"-prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"-prop_checkBashisms13= verify checkBashisms "exec -c env"-prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""-prop_checkBashisms15= verify checkBashisms "let n++"-prop_checkBashisms16= verify checkBashisms "echo $RANDOM"-prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"-prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"-prop_checkBashisms19= verify checkBashisms "foo > file*.txt"-prop_checkBashisms20= verify checkBashisms "read -ra foo"-prop_checkBashisms21= verify checkBashisms "[ -a foo ]"-prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"-prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"-prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"-prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"-prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"-prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"-prop_checkBashisms28= verify checkBashisms "exec {n}>&2"-prop_checkBashisms29= verify checkBashisms "echo ${!var}"-prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""-prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""-prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"-prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"-prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"-prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"-prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"-prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"-prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"-prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"-prop_checkBashisms40= verify checkBashisms "echo $(<file)"-prop_checkBashisms41= verify checkBashisms "echo `<file`"-prop_checkBashisms42= verify checkBashisms "trap foo int"-prop_checkBashisms43= verify checkBashisms "trap foo sigint"-prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"-prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"-prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"-prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"-prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"-prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"-prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"-prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"-prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"-prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"-prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"-prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"-prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"-prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"-prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"+prop_checkBashisms10 = verify checkBashisms "echo ${var:4:12}"+prop_checkBashisms11 = verifyNot checkBashisms "echo ${var:-4}"+prop_checkBashisms12 = verify checkBashisms "echo ${var//foo/bar}"+prop_checkBashisms13 = verify checkBashisms "exec -c env"+prop_checkBashisms14 = verify checkBashisms "echo -n \"Foo: \""+prop_checkBashisms15 = verify checkBashisms "let n++"+prop_checkBashisms16 = verify checkBashisms "echo $RANDOM"+prop_checkBashisms17 = verify checkBashisms "echo $((RANDOM%6+1))"+prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"+prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"+prop_checkBashisms20 = verify checkBashisms "read -ra foo"+prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"+prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"+prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"+prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"+prop_checkBashisms25 = verify checkBashisms "cat < /dev/tcp/host/123"+prop_checkBashisms26 = verify checkBashisms "trap mything ERR SIGTERM"+prop_checkBashisms27 = verify checkBashisms "echo *[^0-9]*"+prop_checkBashisms28 = verify checkBashisms "exec {n}>&2"+prop_checkBashisms29 = verify checkBashisms "echo ${!var}"+prop_checkBashisms30 = verify checkBashisms "printf -v '%s' \"$1\""+prop_checkBashisms31 = verify checkBashisms "printf '%q' \"$1\""+prop_checkBashisms32 = verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"+prop_checkBashisms33 = verify checkBashisms "#!/bin/sh\necho -n foo"+prop_checkBashisms34 = verifyNot checkBashisms "#!/bin/dash\necho -n foo"+prop_checkBashisms35 = verifyNot checkBashisms "#!/bin/dash\nlocal foo"+prop_checkBashisms36 = verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"+prop_checkBashisms37 = verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"+prop_checkBashisms38 = verify checkBashisms "RANDOM=9; echo $RANDOM"+prop_checkBashisms39 = verify checkBashisms "foo-bar() { true; }"+prop_checkBashisms40 = verify checkBashisms "echo $(<file)"+prop_checkBashisms41 = verify checkBashisms "echo `<file`"+prop_checkBashisms42 = verify checkBashisms "trap foo int"+prop_checkBashisms43 = verify checkBashisms "trap foo sigint"+prop_checkBashisms44 = verifyNot checkBashisms "#!/bin/dash\ntrap foo int"+prop_checkBashisms45 = verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"+prop_checkBashisms46 = verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"+prop_checkBashisms47 = verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"+prop_checkBashisms48 = verifyNot checkBashisms "#!/bin/sh\necho $LINENO"+prop_checkBashisms49 = verify checkBashisms "#!/bin/dash\necho $MACHTYPE"+prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"+prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"+prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"+prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"+prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"+prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"+prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"+prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"+prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}"+prop_checkBashisms57 = verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"+prop_checkBashisms58 = verify checkBashisms "#!/bin/sh\nulimit -c 0" prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s" prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p" prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"@@ -224,7 +227,8 @@             warnMsg id 3018 $ filter (/= '|') op ++ " is"     bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"     bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"-    bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"+    bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =+        unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"     bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"     bashism (T_FdRedirect id num _)         | all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"@@ -440,9 +444,9 @@                 _ -> False  prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"-prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"+prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"-prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"+prop_checkEchoSed2b = verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)" checkEchoSed = ForShell [Bash, Ksh] f   where     f (T_Redirecting id lefts r) =@@ -528,11 +532,11 @@     isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re  prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"-prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"+prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"-prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"+prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '" prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
src/ShellCheck/Data.hs view
@@ -2,9 +2,27 @@  import ShellCheck.Interface import Data.Version (showVersion)+++{-+If you are here because you saw an error about Paths_ShellCheck in this file,+simply comment out the import below and define the version as a constant string.++Instead of:++    import Paths_ShellCheck (version)+    shellcheckVersion = showVersion version++Use:++    -- import Paths_ShellCheck (version)+    shellcheckVersion = "kludge"++-}+ import Paths_ShellCheck (version)+shellcheckVersion = showVersion version  -- VERSIONSTRING -shellcheckVersion = showVersion version -- VERSIONSTRING  internalVariables = [     -- Generic@@ -12,23 +30,26 @@      -- Bash     "BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",-    "BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",-    "BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",-    "BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",-    "COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",-    "COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",-    "HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",-    "OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",-    "RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",-    "SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",-    "COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",+    "BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",+    "BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",+    "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",+    "BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",+    "COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",+    "EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",+    "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",+    "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",+    "READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",+    "REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",+    "BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",+    "COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",     "FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",     "HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",-    "IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",-    "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",-    "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",-    "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",-    "TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",+    "IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",+    "LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",+    "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",+    "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",+    "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",+    "auto_resume", "histchars",      -- Other     "USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",@@ -43,13 +64,18 @@     "flags_error", "flags_return"   ] -specialVariablesWithoutSpaces = [-    "$", "-", "?", "!", "#"+specialIntegerVariables = [+    "$", "?", "!", "#"   ]++specialVariablesWithoutSpaces = "-" : specialIntegerVariables+ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [-    "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",-    "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",-    "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"+    "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",+    "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",+    "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",+    "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",+    "HISTSIZE", "LINES"      -- shflags     , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"@@ -95,10 +121,10 @@  nonReadingCommands = [     "alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",-    "cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",+    "cp", "du", "echo", "export", "fg", "fuser", "getconf",     "getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",     "locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",-    "set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"+    "set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"     ]  sampleWords = [@@ -138,5 +164,6 @@         _ -> Nothing  flagsForRead = "sreu:n:N:i:p:a:t:"+flagsForMapfile = "d:n:O:s:u:C:c:t"  declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
+ src/ShellCheck/Debug.hs view
@@ -0,0 +1,313 @@+{-++This file contains useful functions for debugging and developing ShellCheck.++To invoke them interactively, run:++    cabal repl++At the ghci prompt, enter:++    :load ShellCheck.Debug++You can now invoke the functions. Here are some examples:++    shellcheckString "echo $1"+    stringToAst "(( x+1 ))"+    stringToCfg "if foo; then bar; else baz; fi"+    writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"++The latter file can be rendered to png with GraphViz:++    dot -Tpng /tmp/test.dot > /tmp/test.png++To run all unit tests in a module:++    ShellCheck.Parser.runTests+    ShellCheck.Analytics.runTests++To run a specific test:++    :load ShellCheck.Analytics+    prop_checkUuoc3++If you make code changes, reload in seconds at any time with:++    :r++===========================================================================++Crash course in printf debugging in Haskell:++    import Debug.Trace++    greet 0 = return ()+    -- Print when a function is invoked+    greet n | trace ("calling greet " ++ show n) False = undefined+    greet n = do+        putStrLn "Enter name"+        name <- getLine+        -- Print at some point in any monadic function+        traceM $ "user entered " ++ name+        putStrLn $ "Hello " ++ name+        -- Print a value before passing it on+        greet $ traceShowId (n - 1)+++===========================================================================++If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to+debug all of ShellCheck including I/O, you may see an error like this:++    src/ShellCheck/Data.hs:5:1: error:+        Could not load module ‘Paths_ShellCheck’+    it is a hidden module in the package ‘ShellCheck-0.8.0’++This can easily be circumvented by running `./setgitversion` or manually+editing src/ShellCheck/Data.hs to replace the auto-deduced version number+with a constant string as indicated.++Afterwards, you can run the ShellCheck tool, as if from the shell, with:++    $ ghci shellcheck.hs+    ghci> runMain ["-x", "file.sh"]++-}++module ShellCheck.Debug () where++import ShellCheck.Analyzer+import ShellCheck.AST+import ShellCheck.CFG+import ShellCheck.Checker+import ShellCheck.CFGAnalysis as CF+import ShellCheck.Interface+import ShellCheck.Parser+import ShellCheck.Prelude++import Control.Monad+import Control.Monad.Identity+import Control.Monad.RWS+import Control.Monad.Writer+import Data.Graph.Inductive.Graph as G+import Data.List+import Data.Maybe+import qualified Data.Map as M+import qualified Data.Set as S+++-- Run all of ShellCheck (minus output formatters)+shellcheckString :: String -> CheckResult+shellcheckString scriptString =+    runIdentity $ checkScript dummySystemInterface checkSpec+  where+    checkSpec :: CheckSpec+    checkSpec = emptyCheckSpec {+        csScript = scriptString+    }++dummySystemInterface :: SystemInterface Identity+dummySystemInterface = mockedSystemInterface [+    -- A tiny, fake filesystem for sourced files+    ("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),+    ("lib/mylib2.sh", "bar=42")+    ]++-- Parameters used when generating Control Flow Graphs+cfgParams :: CFGParameters+cfgParams = CFGParameters {+    cfLastpipe = False,+    cfPipefail = False+}++-- An example script to play with+exampleScript :: String+exampleScript = unlines [+    "#!/bin/sh",+    "count=0",+    "for file in *",+    "do",+    "  (( count++ ))",+    "done",+    "echo $count"+    ]++-- Parse the script string into ShellCheck's ParseResult+parseScriptString :: String -> ParseResult+parseScriptString scriptString =+    runIdentity $ parseScript dummySystemInterface parseSpec+  where+    parseSpec :: ParseSpec+    parseSpec = newParseSpec {+        psFilename = "myscript",+        psScript = scriptString+    }+++-- Parse the script string into an Abstract Syntax Tree+stringToAst :: String -> Token+stringToAst scriptString =+    case maybeRoot of+        Just root -> root+        Nothing -> error $ "Script failed to parse: " ++ show parserWarnings+  where+    parseResult :: ParseResult+    parseResult = parseScriptString scriptString++    maybeRoot :: Maybe Token+    maybeRoot = prRoot parseResult++    parserWarnings :: [PositionedComment]+    parserWarnings = prComments parseResult+++astToCfgResult :: Token -> CFGResult+astToCfgResult = buildGraph cfgParams++astToDfa :: Token -> CFGAnalysis+astToDfa = analyzeControlFlow cfgParams++astToCfg :: Token -> CFGraph+astToCfg = cfGraph . astToCfgResult++stringToCfg :: String -> CFGraph+stringToCfg = astToCfg . stringToAst++stringToDfa :: String -> CFGAnalysis+stringToDfa = astToDfa . stringToAst++cfgToGraphViz :: CFGraph -> String+cfgToGraphViz = cfgToGraphVizWith show++stringToCfgViz :: String -> String+stringToCfgViz = cfgToGraphViz . stringToCfg++stringToDfaViz :: String -> String+stringToDfaViz = dfaToGraphViz . stringToDfa++-- Dump a Control Flow Graph as GraphViz with extended information+stringToDetailedCfgViz :: String -> String+stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph+  where+    ast :: Token+    ast = stringToAst scriptString++    cfgResult :: CFGResult+    cfgResult = astToCfgResult ast++    graph :: CFGraph+    graph = cfGraph cfgResult++    idToToken :: M.Map Id Token+    idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast++    idToNode :: M.Map Id (Node, Node)+    idToNode = cfIdToRange cfgResult++    nodeToStartIds :: M.Map Node (S.Set Id)+    nodeToStartIds =+        M.fromListWith S.union $+            map (\(id, (start, _)) -> (start, S.singleton id)) $+                M.toList idToNode++    nodeToEndIds :: M.Map Node (S.Set Id)+    nodeToEndIds =+        M.fromListWith S.union $+            map (\(id, (_, end)) -> (end, S.singleton id)) $+                M.toList idToNode++    formatId :: Id -> String+    formatId id = fromMaybe ("Unknown " ++ show id) $ do+        (OuterToken _ token) <- M.lookup id idToToken+        firstWord <- words (show token) !!! 0+        -- Strip off "Inner_"+        (_ : tokenName) <- return $ dropWhile (/= '_') firstWord+        return $ tokenName ++ " " ++ show id++    formatGroup :: S.Set Id -> String+    formatGroup set = intercalate ", " $ map formatId $ S.toList set++    nodeLabel (node, label) = unlines [+        show node ++ ". " ++ show label,+        "Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),+        "End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)+        ]+++-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz+dfaToGraphViz :: CF.CFGAnalysis -> String+dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis+  where+    label (node, label) =+        let+            desc = show node ++ ". " ++ show label+        in+            fromMaybe ("No DFA available\n\n" ++ desc) $ do+                (pre, post) <- M.lookup node $ CF.nodeToData analysis+                return $ unlines [+                    "Precondition: " ++ show pre,+                    "",+                    desc,+                    "",+                    "Postcondition: " ++ show post+                    ]+++-- Dump an Control Flow Graph to GraphViz with a given node formatter+cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String+cfgToGraphVizWith nodeLabel graph = concat [+    "digraph {\n",+    concatMap dumpNode (labNodes graph),+    concatMap dumpLink (labEdges graph),+    tagVizEntries graph,+    "}\n"+    ]+  where+    dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"+    dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ)  ++ "]\n"+    edgeStyle CFEFlow = "solid"+    edgeStyle CFEExit = "bold"+    edgeStyle CFEFalseFlow = "dotted"++quoteViz str = "\"" ++ escapeViz str ++ "\""+escapeViz [] = []+escapeViz (c:rest) =+    case c of+        '\"' -> '\\' : '\"' : escapeViz rest+        '\n' -> '\\' : 'l' : escapeViz rest+        '\\' -> '\\' : '\\' : escapeViz rest+        _ -> c : escapeViz rest+++-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format+astToGraphViz :: Token -> String+astToGraphViz token = concat [+    "digraph {\n",+    formatTree token,+    "}\n"+    ]+  where+    formatTree :: Token -> String+    formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []++    push :: Token -> RWS () String [Int] ()+    push (OuterToken (Id n) inner) = do+        stack <- get+        put (n : stack)+        case stack of+            [] -> return ()+            (top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"+        tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"++    pop :: Token -> RWS () String [Int] ()+    pop _ = modify tail+++-- For each entry point, set the rank so that they'll align in the graph+tagVizEntries :: CFGraph -> String+tagVizEntries graph = "{ rank=same " ++ rank ++ " }"+  where+    entries = mapMaybe find $ labNodes graph+    find (node, CFEntryPoint name) = return (node, name)+    find _ = Nothing+    rank = unwords $ map (\(c, _) -> show c) entries
src/ShellCheck/Fixer.hs view
@@ -22,6 +22,8 @@ module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where  import ShellCheck.Interface+import ShellCheck.Prelude+import Control.Monad import Control.Monad.State import Data.Array import Data.List@@ -35,7 +37,7 @@     end     :: a -> Position     overlap :: a -> a -> Bool     overlap x y =-        (yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)+        xEnd > yStart && yEnd > xStart         where             yStart = start y             yEnd = end y@@ -86,6 +88,7 @@ instance Monoid Fix where     mempty = newFix     mappend = (<>)+    mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap  instance Semigroup Fix where     f1 <> f2 =@@ -228,7 +231,7 @@      let (l1, l2) = tmap posLine originalPos in         when (l1 /= 1 || l2 /= 1) $-            error "ShellCheck internal error, please report: bad cross-line fix"+            error $ pleaseReport "bad cross-line fix"      let replacer = repString rep     let shift = (length replacer) - (oldEnd - oldStart)
src/ShellCheck/Formatter/CheckStyle.hs view
@@ -88,7 +88,7 @@ attr s v = concat [ s, "='", escape v, "' " ] escape = concatMap escape' escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"-isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]+isOk x = any ($ x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]  severity "error" = "error" severity "warning" = "warning"
src/ShellCheck/Formatter/Diff.hs view
@@ -203,10 +203,9 @@ buildFixMap :: [Fix] -> M.Map String Fix buildFixMap fixes = perFile   where-    splitFixes = concatMap splitFixByFile fixes+    splitFixes = splitFixByFile $ mconcat fixes     perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes --- There are currently no multi-file fixes, but let's handle it anyways splitFixByFile :: Fix -> [Fix] splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)   where
src/ShellCheck/Formatter/JSON.hs view
@@ -23,6 +23,7 @@ import ShellCheck.Interface import ShellCheck.Formatter.Format +import Control.DeepSeq import Data.Aeson import Data.IORef import Data.Monoid@@ -103,7 +104,7 @@     comments = crComments cr     groups = groupWith sourceFile comments     f :: [PositionedComment] -> IO ()-    f group = modifyIORef ref (\x -> comments ++ x)+    f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)  finish ref = do     list <- readIORef ref
src/ShellCheck/Formatter/JSON1.hs view
@@ -23,6 +23,7 @@ import ShellCheck.Interface import ShellCheck.Formatter.Format +import Control.DeepSeq import Data.Aeson import Data.IORef import Data.Monoid@@ -120,7 +121,7 @@         result <- siReadFile sys (Just True) filename         let contents = either (const "") id result         let comments' = makeNonVirtual comments contents-        modifyIORef ref (\x -> comments' ++ x)+        deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)  finish ref = do     list <- readIORef ref
src/ShellCheck/Formatter/TTY.hs view
@@ -23,6 +23,7 @@ import ShellCheck.Interface import ShellCheck.Formatter.Format +import Control.DeepSeq import Control.Monad import Data.Array import Data.Foldable@@ -88,7 +89,7 @@ appendComments errRef comments max = do     previous <- readIORef errRef     let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments-    writeIORef errRef . take max . nubBy equal . sort $ previous ++ current+    writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current   where     fst3 (x,_,_) = x     equal x y = fst3 x == fst3 y
src/ShellCheck/Parser.hs view
@@ -1,5 +1,5 @@ {--    Copyright 2012-2021 Vidar Holen+    Copyright 2012-2022 Vidar Holen      This file is part of ShellCheck.     https://www.shellcheck.net@@ -27,6 +27,7 @@ import ShellCheck.ASTLib hiding (runTests) import ShellCheck.Data import ShellCheck.Interface+import ShellCheck.Prelude  import Control.Applicative ((<*), (*>)) import Control.Monad@@ -37,7 +38,6 @@ import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find) import Data.Maybe import Data.Monoid-import Debug.Trace -- STRIP import GHC.Exts (sortWith) import Prelude hiding (readList) import System.IO@@ -46,7 +46,7 @@ import Text.Parsec.Pos import qualified Control.Monad.Reader as Mr import qualified Control.Monad.State as Ms-import qualified Data.Map as Map+import qualified Data.Map.Strict as Map  import Test.QuickCheck.All (quickCheckAll) @@ -210,7 +210,7 @@ -- Get the span covered by an id getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos) getSpanForId id =-    Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>+    Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$>         getMap  -- Create a new id with the same span as an existing one@@ -457,8 +457,8 @@     pos <- getPosition     withContext (ContextName pos s) p -withAnnotations anns =-    withContext (ContextAnnotation anns)+withAnnotations anns p =+    if null anns then p else withContext (ContextAnnotation anns) p  readConditionContents single =     readCondContents `attempting` lookAhead (do@@ -556,7 +556,7 @@         notFollowedBy2 (try (spacing >> string "]"))         x <- readNormalWord         pos <- getPosition-        when (endedWith "]" x && notArrayIndex x) $ do+        when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do             parseProblemAt pos ErrorC 1020 $                 "You need a space before the " ++ (if single then "]" else "]]") ++ "."             fail "Missing space before ]"@@ -572,6 +572,7 @@             endedWith _ _ = False             notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["             notArrayIndex _ = True+            containsLiteral x s = s `isInfixOf` onlyLiteralString x      readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True @@ -717,20 +718,20 @@ prop_a7 = isOk readArithmeticContents "3*2**10" prop_a8 = isOk readArithmeticContents "3" prop_a9 = isOk readArithmeticContents "a^!-b"-prop_a10= isOk readArithmeticContents "! $?"-prop_a11= isOk readArithmeticContents "10#08 * 16#f"-prop_a12= isOk readArithmeticContents "\"$((3+2))\" + '37'"-prop_a13= isOk readArithmeticContents "foo[9*y+x]++"-prop_a14= isOk readArithmeticContents "1+`echo 2`"-prop_a15= isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"-prop_a16= isOk readArithmeticContents "$foo$bar"-prop_a17= isOk readArithmeticContents "i<(0+(1+1))"-prop_a18= isOk readArithmeticContents "a?b:c"-prop_a19= isOk readArithmeticContents "\\\n3 +\\\n  2"-prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"-prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"-prop_a22= isOk readArithmeticContents "!!a"-prop_a23= isOk readArithmeticContents "~0"+prop_a10 = isOk readArithmeticContents "! $?"+prop_a11 = isOk readArithmeticContents "10#08 * 16#f"+prop_a12 = isOk readArithmeticContents "\"$((3+2))\" + '37'"+prop_a13 = isOk readArithmeticContents "foo[9*y+x]++"+prop_a14 = isOk readArithmeticContents "1+`echo 2`"+prop_a15 = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"+prop_a16 = isOk readArithmeticContents "$foo$bar"+prop_a17 = isOk readArithmeticContents "i<(0+(1+1))"+prop_a18 = isOk readArithmeticContents "a?b:c"+prop_a19 = isOk readArithmeticContents "\\\n3 +\\\n  2"+prop_a20 = isOk readArithmeticContents "a ? b ? c : d : e"+prop_a21 = isOk readArithmeticContents "a ? b : c ? d : e"+prop_a22 = isOk readArithmeticContents "!!a"+prop_a23 = isOk readArithmeticContents "~0" readArithmeticContents :: Monad m => SCParser m Token readArithmeticContents =     readSequence@@ -819,11 +820,13 @@         return $ TA_Expansion id pieces      readGroup = do+        start <- startSpan         char '('         s <- readSequence         char ')'+        id <- endSpan start         spacing-        return s+        return $ TA_Parentesis id s      readArithTerm = readGroup <|> readVariable <|> readExpansion @@ -923,8 +926,8 @@ prop_readCondition8  = isOk readCondition "[[ $l =~ ogg|flac ]]" prop_readCondition9  = isOk readCondition "[ foo -a -f bar ]" prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]"-prop_readCondition10a= isOk readCondition "[[\na == b  ||\nc == d ]]"-prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"+prop_readCondition10a = isOk readCondition "[[\na == b  ||\nc == d ]]"+prop_readCondition10b = isOk readCondition "[[ a == b\n||\nc == d ]]" prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]" prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]" prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"@@ -941,6 +944,9 @@ prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]" prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar" prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"+prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]"+prop_readCondition29 = isOk readCondition "[[ x = [*] ]]"+ readCondition = called "test expression" $ do     opos <- getPosition     start <- startSpan@@ -985,6 +991,10 @@ prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n" prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n" prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n"+prop_readAnnotation9 = isOk readAnnotation "# shellcheck source='foo bar' source-path=\"baz etc\"\n"+prop_readAnnotation10 = isOk readAnnotation "# shellcheck disable='SC1234,SC2345' enable=\"foo\" shell='bash'\n"+prop_readAnnotation11 = isOk (readAnnotationWithoutPrefix False) "external-sources='true'"+ readAnnotation = called "shellcheck directive" $ do     try readAnnotationPrefix     many1 linewhitespace@@ -1000,12 +1010,19 @@     many linewhitespace     return $ concat values   where+    plainOrQuoted p = quoted p <|> p+    quoted p = do+        c <- oneOf "'\""+        start <- getPosition+        str <- many1 $ noneOf (c:"\n")+        char c <|> fail "Missing terminating quote for directive."+        subParse start p str     readKey = do         keyPos <- getPosition         key <- many1 (letter <|> char '-')         char '=' <|> fail "Expected '=' after directive key"         annotations <- case key of-            "disable" -> readElement `sepBy` char ','+            "disable" -> plainOrQuoted $ readElement `sepBy` char ','               where                 readElement = readRange <|> readAll                 readAll = do@@ -1020,21 +1037,21 @@                     int <- many1 digit                     return $ read int -            "enable" -> readName `sepBy` char ','+            "enable" -> plainOrQuoted $ readName `sepBy` char ','               where                 readName = EnableComment <$> many1 (letter <|> char '-')              "source" -> do-                filename <- many1 $ noneOf " \n"+                filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")                 return [SourceOverride filename]              "source-path" -> do-                dirname <- many1 $ noneOf " \n"+                dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")                 return [SourcePath dirname]              "shell" -> do                 pos <- getPosition-                shell <- many1 $ noneOf " \n"+                shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")                 when (isNothing $ shellForExecutable shell) $                     parseNoteAt pos ErrorC 1103                         "This shell type is unknown. Use e.g. sh or bash."@@ -1042,7 +1059,7 @@              "external-sources" -> do                 pos <- getPosition-                value <- many1 letter+                value <- plainOrQuoted $ many1 letter                 case value of                     "true" ->                         if sandboxed@@ -1696,9 +1713,9 @@     id <- endSpan start     return $ T_DollarBraced id True word -prop_readDollarExpansion1= isOk readDollarExpansion "$(echo foo; ls\n)"-prop_readDollarExpansion2= isOk readDollarExpansion "$(  )"-prop_readDollarExpansion3= isOk readDollarExpansion "$( command \n#comment \n)"+prop_readDollarExpansion1 = isOk readDollarExpansion "$(echo foo; ls\n)"+prop_readDollarExpansion2 = isOk readDollarExpansion "$(  )"+prop_readDollarExpansion3 = isOk readDollarExpansion "$( command \n#comment \n)" readDollarExpansion = called "command expansion" $ do     start <- startSpan     try (string "$(")@@ -1790,17 +1807,17 @@ prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo" prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo" prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"-prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"-prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"-prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"-prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"-prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"-prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"-prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"-prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n  foo\n foo\n"-prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"-prop_readHereDoc20= isWarning readScript "cat << foo\n  foo\n()\nfoo\n"-prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n  foo\n()\nfoo\n"+prop_readHereDoc10 = isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"+prop_readHereDoc11 = isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"+prop_readHereDoc12 = isOk readScript "cat << foo|cat\nbar\nfoo"+prop_readHereDoc13 = isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"+prop_readHereDoc14 = isWarning readScript "cat << foo\nbar\nfoo \n"+prop_readHereDoc15 = isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"+prop_readHereDoc16 = isOk readScript "cat <<- ' foo'\nbar\n foo\n"+prop_readHereDoc17 = isWarning readScript "cat <<- ' foo'\nbar\n  foo\n foo\n"+prop_readHereDoc18 = isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"+prop_readHereDoc20 = isWarning readScript "cat << foo\n  foo\n()\nfoo\n"+prop_readHereDoc21 = isOk readScript "# shellcheck disable=SC1039\ncat << foo\n  foo\n()\nfoo\n" prop_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n" prop_readHereDoc23 = isNotOk readScript "cat << foo \r\ncow\r\nfoo\r\n" readHereDoc = called "here document" $ do@@ -1914,7 +1931,7 @@                         -- The end token is just a prefix                         skipLine                     | hasTrailer ->-                        error "ShellCheck bug, please report (here doc trailer)."+                        error $ pleaseReport "unexpected heredoc trailer"                      -- The following cases assume no trailing text:                     | dashed == Undashed && (not $ null leadingSpace) -> do@@ -2095,6 +2112,7 @@ prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo" prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo" prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"+prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT" readSimpleCommand = called "simple command" $ do     prefix <- option [] readCmdPrefix     skipAnnotationAndWarn@@ -2124,9 +2142,12 @@             id2 <- getNewIdFor id1              let result = makeSimpleCommand id1 id2 prefix [cmd] suffix-            if isCommand ["source", "."] cmd-                then readSource result-                else return result+            case () of+                _ | isCommand ["source", "."] cmd -> readSource result+                _ | isCommand ["trap"] cmd -> do+                        syntaxCheckTrap result+                        return result+                _ -> return result   where     isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings     isCommand _ _ = False@@ -2146,6 +2167,17 @@                     parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."             _ -> return () +    syntaxCheckTrap cmd =+        case cmd of+            (T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:arg:_))) -> checkArg arg (getLiteralString arg)+            _ -> return ()+      where+        checkArg _ Nothing = return ()+        checkArg arg (Just ('-':_)) = return ()+        checkArg arg (Just str) = do+            (start,end) <- getSpanForId (getId arg)+            subParse start (tryWithErrors (readCompoundListOrEmpty >> verifyEof) <|> return ()) str+     commentWarning id =         parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh." @@ -2282,7 +2314,7 @@         parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."      andOr <- withAnnotations annotations $-        chainr1 readPipeline $ do+        chainl1 readPipeline $ do             op <- g_AND_IF <|> g_OR_IF             readLineBreak             return $ case op of T_AND_IF id -> T_AndIf id@@ -2483,17 +2515,30 @@     spacing     return $ T_BraceGroup id list -prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n  true\n}"+prop_readBatsTest1 = isOk readBatsTest "@test 'can parse' {\n  true\n}"+prop_readBatsTest2 = isOk readBatsTest "@test random text !(@*$Y&! {\n  true\n}"+prop_readBatsTest3 = isOk readBatsTest "@test foo { bar { baz {\n  true\n}"+prop_readBatsTest4 = isNotOk readBatsTest "@test foo \n{\n true\n}" readBatsTest = called "bats @test" $ do     start <- startSpan-    try $ string "@test"+    try $ string "@test "     spacing-    name <- readNormalWord+    name <- readBatsName     spacing     test <- readBraceGroup     id <- endSpan start     return $ T_BatsTest id name test+  where+    readBatsName = do+        line <- try . lookAhead $ many1 $ noneOf "\n"+        let name = reverse $ f $ reverse line+        string name +    -- We want everything before the last " {" in a string, so we find everything after "{ " in its reverse+    f ('{':' ':rest) = dropWhile isSpace rest+    f (a:rest) = f rest+    f [] = ""+ prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done" readWhileClause = called "while loop" $ do     start <- startSpan@@ -2521,7 +2566,7 @@         parseProblem ErrorC 1058 "Expected 'do'."         return "Expected 'do'" -    acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."+    acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it."     allspacing      optional (do@@ -2550,9 +2595,9 @@ prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone" prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone" prop_readForClause9 = isOk readForClause "for i do true; done"-prop_readForClause10= isOk readForClause "for ((;;)) { true; }"-prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done"-prop_readForClause13= isOk readForClause "for foo\nin\\\n  bar\\\n  baz\ndo true; done"+prop_readForClause10 = isOk readForClause "for ((;;)) { true; }"+prop_readForClause12 = isWarning readForClause "for $a in *; do echo \"$a\"; done"+prop_readForClause13 = isOk readForClause "for foo\nin\\\n  bar\\\n  baz\ndo true; done" readForClause = called "for loop" $ do     pos <- getPosition     (T_For id) <- g_For@@ -2684,10 +2729,10 @@ prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }" prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)" prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"-prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"-prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"-prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }"-prop_readFunctionDefinition13= isOk readFunctionDefinition "@require(){ true; }"+prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { true; }"+prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}"+prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }"+prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }" readFunctionDefinition = called "function" $ do     start <- startSpan     functionSignature <- try readFunctionSignature@@ -2885,14 +2930,14 @@ prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42" prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42" prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "-prop_readAssignmentWord9a= isOk readAssignmentWord "foo="-prop_readAssignmentWord9b= isOk readAssignmentWord "foo=  "-prop_readAssignmentWord9c= isOk readAssignmentWord "foo=  #bar"-prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"-prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"-prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"-prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"-prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"+prop_readAssignmentWord9a = isOk readAssignmentWord "foo="+prop_readAssignmentWord9b = isOk readAssignmentWord "foo=  "+prop_readAssignmentWord9c = isOk readAssignmentWord "foo=  #bar"+prop_readAssignmentWord11 = isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"+prop_readAssignmentWord12 = isOk readAssignmentWord "a[b <<= 3 + c]='thing'"+prop_readAssignmentWord13 = isOk readAssignmentWord "var=( (1 2) (3 4) )"+prop_readAssignmentWord14 = isOk readAssignmentWord "var=( 1 [2]=(3 4) )"+prop_readAssignmentWord15 = isOk readAssignmentWord "var=(1 [2]=(3 4))" readAssignmentWord = readAssignmentWordExt True readWellFormedAssignment = readAssignmentWordExt False readAssignmentWordExt lenient = called "variable assignment" $ do@@ -3240,45 +3285,52 @@ prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=(" prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n" prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"+prop_readScript7 = isOk readScript "#!/bin/zsh\n# shellcheck disable=SC1071\nfor f (a b); echo $f\n" readScriptFile sourced = do     start <- startSpan     pos <- getPosition-    optional $ do-        readUtf8Bom-        parseProblem ErrorC 1082-            "This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."-    shebang <- readShebang <|> readEmptyLiteral-    let (T_Literal _ shebangString) = shebang-    allspacing-    annotationStart <- startSpan-    fileAnnotations <- readAnnotations     rcAnnotations <- if sourced                      then return []                      else do                         filename <- Mr.asks currentFilename                         readConfigFile filename-    let annotations = fileAnnotations ++ rcAnnotations-    annotationId <- endSpan annotationStart-    let shellAnnotationSpecified =-            any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations-    shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride-    let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified -    unless ignoreShebang $-        verifyShebang pos (executableFromShebang shebangString)-    if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False-      then do-            commands <- withAnnotations annotations readCompoundListOrEmpty-            id <- endSpan start-            verifyEof-            let script = T_Annotation annotationId annotations $-                            T_Script id shebang commands-            reparseIndices script-        else do-            many anyChar-            id <- endSpan start-            return $ T_Script id shebang []+    -- Put the rc annotations on the stack so that one can ignore e.g. SC1084 in .shellcheckrc+    withAnnotations rcAnnotations $ do+        hasBom <- wasIncluded readUtf8Bom+        shebang <- readShebang <|> readEmptyLiteral+        let (T_Literal _ shebangString) = shebang+        allspacing+        annotationStart <- startSpan+        fileAnnotations <- readAnnotations +        -- Similarly put the filewide annotations on the stack to allow earlier suppression+        withAnnotations fileAnnotations $ do+            when (hasBom) $+                parseProblemAt pos ErrorC 1082+                    "This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."+            let annotations = fileAnnotations ++ rcAnnotations+            annotationId <- endSpan annotationStart+            let shellAnnotationSpecified =+                    any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations+            shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride+            let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified++            unless ignoreShebang $+                verifyShebang pos (executableFromShebang shebangString)+            if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False+              then do+                    commands <- readCompoundListOrEmpty+                    id <- endSpan start+                    verifyEof+                    let script = T_Annotation annotationId annotations $+                                    T_Script id shebang commands+                    reparseIndices script+                else do+                    many anyChar+                    id <- endSpan start+                    return $ T_Script id shebang []+   where     verifyShebang pos s = do         case isValidShell s of@@ -3370,16 +3422,6 @@             return $ Just . null $ parseNotes userState ++ parseProblems systemState         (Left _, _) -> return Nothing --- For printf debugging: print the value of an expression--- Example: return $ dump $ T_Literal id [c]-dump :: Show a => a -> a     -- STRIP-dump x = trace (show x) x    -- STRIP---- Like above, but print a specific expression:--- Example: return $ dumps ("Returning: " ++ [c])  $ T_Literal id [c]-dumps :: Show x => x -> a -> a -- STRIP-dumps t = trace (show t)       -- STRIP- parseWithNotes parser = do     item <- parser     state <- getState@@ -3459,9 +3501,9 @@  -- Go over all T_UnparsedIndex and reparse them as either arithmetic or text -- depending on declare -A statements.-reparseIndices root =-   analyze blank blank f root+reparseIndices root = process root   where+    process = analyze blank blank f     associative = getAssociativeArrays root     isAssociative s = s `elem` associative     f (T_Assignment id mode name indices value) = do@@ -3486,8 +3528,9 @@      fixAssignmentIndex name word =         case word of-            T_UnparsedIndex id pos src ->-                parsed name pos src+            T_UnparsedIndex id pos src -> do+                idx <- parsed name pos src+                process idx -- Recursively parse for cases like x[y[z=1]]=1             _ -> return word      parsed name pos src =
+ src/ShellCheck/Prelude.hs view
@@ -0,0 +1,51 @@+{-+    Copyright 2022 Vidar Holen++    This file is part of ShellCheck.+    https://www.shellcheck.net++    ShellCheck is free software: you can redistribute it and/or modify+    it under the terms of the GNU General Public License as published by+    the Free Software Foundation, either version 3 of the License, or+    (at your option) any later version.++    ShellCheck is distributed in the hope that it will be useful,+    but WITHOUT ANY WARRANTY; without even the implied warranty of+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the+    GNU General Public License for more details.++    You should have received a copy of the GNU General Public License+    along with this program.  If not, see <https://www.gnu.org/licenses/>.+-}++-- Generic basic utility functions+module ShellCheck.Prelude where++import Data.Semigroup+++-- Get element 0 or a default. Like `head` but safe.+headOrDefault _ (a:_) = a+headOrDefault def _   = def++-- Get the last element or a default. Like `last` but safe.+lastOrDefault def [] = def+lastOrDefault _ list = last list++--- Get element n of a list, or Nothing. Like `!!` but safe.+(!!!) list i =+    case drop i list of+        []    -> Nothing+        (r:_) -> Just r+++-- Like mconcat but for Semigroups+sconcat1 :: (Semigroup t) => [t] -> t+sconcat1 [x] = x+sconcat1 (x:xs) = x <> sconcat1 xs++sconcatOrDefault def [] = def+sconcatOrDefault _ list = sconcat1 list++-- For more actionable "impossible" errors+pleaseReport str = "ShellCheck internal error, please report: " ++ str
test/shellcheck.hs view
@@ -5,8 +5,11 @@ import qualified ShellCheck.Analytics import qualified ShellCheck.AnalyzerLib import qualified ShellCheck.ASTLib+import qualified ShellCheck.CFG+import qualified ShellCheck.CFGAnalysis import qualified ShellCheck.Checker import qualified ShellCheck.Checks.Commands+import qualified ShellCheck.Checks.ControlFlow import qualified ShellCheck.Checks.Custom import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Fixer@@ -19,8 +22,11 @@         ShellCheck.Analytics.runTests         ,ShellCheck.AnalyzerLib.runTests         ,ShellCheck.ASTLib.runTests+        ,ShellCheck.CFG.runTests+        ,ShellCheck.CFGAnalysis.runTests         ,ShellCheck.Checker.runTests         ,ShellCheck.Checks.Commands.runTests+        ,ShellCheck.Checks.ControlFlow.runTests         ,ShellCheck.Checks.Custom.runTests         ,ShellCheck.Checks.ShellSupport.runTests         ,ShellCheck.Fixer.runTests