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 +22/−0
- LICENSE +0/−10
- ShellCheck.cabal +43/−27
- shellcheck.1.md +4/−1
- shellcheck.hs +3/−1
- src/ShellCheck/AST.hs +4/−2
- src/ShellCheck/ASTLib.hs +147/−8
- src/ShellCheck/Analytics.hs +502/−349
- src/ShellCheck/Analyzer.hs +7/−4
- src/ShellCheck/AnalyzerLib.hs +34/−145
- src/ShellCheck/CFG.hs +1307/−0
- src/ShellCheck/CFGAnalysis.hs +1424/−0
- src/ShellCheck/Checker.hs +27/−3
- src/ShellCheck/Checks/Commands.hs +145/−64
- src/ShellCheck/Checks/ControlFlow.hs +101/−0
- src/ShellCheck/Checks/ShellSupport.hs +58/−54
- src/ShellCheck/Data.hs +50/−23
- src/ShellCheck/Debug.hs +313/−0
- src/ShellCheck/Fixer.hs +5/−2
- src/ShellCheck/Formatter/CheckStyle.hs +1/−1
- src/ShellCheck/Formatter/Diff.hs +1/−2
- src/ShellCheck/Formatter/JSON.hs +2/−1
- src/ShellCheck/Formatter/JSON1.hs +2/−1
- src/ShellCheck/Formatter/TTY.hs +2/−1
- src/ShellCheck/Parser.hs +154/−111
- src/ShellCheck/Prelude.hs +51/−0
- test/shellcheck.hs +6/−0
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