From 0138a6fafcbf107583753d87710f29cdebf7a80f Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 13 Aug 2023 17:49:36 -0700 Subject: [PATCH] Example plumbing for Portage variables --- shellcheck.hs | 16 ++++++- src/ShellCheck/Analytics.hs | 4 +- src/ShellCheck/Analyzer.hs | 16 +++---- src/ShellCheck/AnalyzerLib.hs | 87 ++++++++++++++++++++--------------- src/ShellCheck/CFG.hs | 4 +- src/ShellCheck/CFGAnalysis.hs | 9 ++-- src/ShellCheck/Checker.hs | 9 ++-- src/ShellCheck/Debug.hs | 3 +- src/ShellCheck/Interface.hs | 7 ++- 9 files changed, 96 insertions(+), 59 deletions(-) diff --git a/shellcheck.hs b/shellcheck.hs index 6be9bb1..c306440 100644 --- a/shellcheck.hs +++ b/shellcheck.hs @@ -396,10 +396,12 @@ ioInterface options files = do inputs <- mapM normalize files cache <- newIORef emptyCache configCache <- newIORef ("", Nothing) + portageVars <- newIORef Nothing return (newSystemInterface :: SystemInterface IO) { siReadFile = get cache inputs, siFindSource = findSourceFile inputs (sourcePaths options), - siGetConfig = getConfig configCache + siGetConfig = getConfig configCache, + siGetPortageVariables = getOrLoadPortage portageVars } where emptyCache :: Map.Map FilePath String @@ -523,6 +525,18 @@ ioInterface options files = do ("SCRIPTDIR":rest) -> joinPath (scriptdir:rest) _ -> str + getOrLoadPortage cache = do + x <- readIORef cache + case x of + Just m -> do + hPutStrLn stderr "Reusing previous Portage variables" + return m + Nothing -> do + hPutStrLn stderr "Computing Portage variables" + vars <- return $ Map.fromList [("foo", ["bar", "baz"])] -- TODO: Actually read the variables + writeIORef cache $ Just vars + return vars + inputFile file = do (handle, shouldCache) <- if file == "-" diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 7e8b510..255c63d 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -314,7 +314,7 @@ runAndGetComments f s = do let pr = pScript s root <- prRoot pr let spec = defaultSpec pr - let params = makeParameters spec + let params = runIdentity $ makeParameters (mockedSystemInterface []) spec return $ filterByAnnotation spec params $ f params root @@ -2451,7 +2451,7 @@ checkUnassignedReferences = checkUnassignedReferences' False checkUnassignedReferences' includeGlobals params t = warnings where (readMap, writeMap) = execState (mapM tally $ variableFlow params) (Map.empty, Map.empty) - defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) internalVariables + defaultAssigned = Map.fromList $ map (\a -> (a, ())) $ filter (not . null) (internalVariables ++ additionalKnownVariables params) tally (Assignment (_, _, name, _)) = modify (\(read, written) -> (read, Map.insert name () written)) diff --git a/src/ShellCheck/Analyzer.hs b/src/ShellCheck/Analyzer.hs index 53717ed..b6cd172 100644 --- a/src/ShellCheck/Analyzer.hs +++ b/src/ShellCheck/Analyzer.hs @@ -31,14 +31,14 @@ import qualified ShellCheck.Checks.ShellSupport -- TODO: Clean up the cruft this is layered on -analyzeScript :: AnalysisSpec -> AnalysisResult -analyzeScript spec = newAnalysisResult { - arComments = - filterByAnnotation spec params . nub $ - runChecker params (checkers spec params) -} - where - params = makeParameters spec +analyzeScript :: Monad m => SystemInterface m -> AnalysisSpec -> m AnalysisResult +analyzeScript sys spec = do + params <- makeParameters sys spec + return $ newAnalysisResult { + arComments = + filterByAnnotation spec params . nub $ + runChecker params (checkers spec params) + } checkers spec params = mconcat $ map ($ params) [ ShellCheck.Analytics.checker spec, diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index ca928fd..137c098 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -103,7 +103,9 @@ data Parameters = Parameters { -- map from token id to start and end position tokenPositions :: Map.Map Id (Position, Position), -- Result from Control Flow Graph analysis (including data flow analysis) - cfgAnalysis :: CF.CFGAnalysis + cfgAnalysis :: CF.CFGAnalysis, + -- A set of additional variables known to be set (TODO: make this more general?) + additionalKnownVariables :: [String] } deriving (Show) -- TODO: Cache results of common AST ops here @@ -152,7 +154,7 @@ producesComments c s = do let pr = pScript s prRoot pr let spec = defaultSpec pr - let params = makeParameters spec + let params = runIdentity $ makeParameters (mockedSystemInterface []) spec return . not . null $ filterByAnnotation spec params $ runChecker params c makeComment :: Severity -> Id -> Code -> String -> TokenComment @@ -196,41 +198,54 @@ makeCommentWithFix severity id code str fix = } in force withFix -makeParameters spec = params +makeParameters :: Monad m => SystemInterface m -> AnalysisSpec -> m Parameters +makeParameters sys spec = do + extraVars <- + case shell of + Bash -> do -- TODO: EBuild type + vars <- siGetPortageVariables sys + return $ Map.findWithDefault [] "foo" vars -- TODO: Determine what to look up in map + _ -> return [] + return $ makeParams extraVars where - params = Parameters { - rootNode = root, - shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, - hasSetE = containsSetE root, - hasLastpipe = - case shellType params of - Bash -> isOptionSet "lastpipe" root - Dash -> False - Sh -> False - Ksh -> True, - hasInheritErrexit = - case shellType params of - Bash -> isOptionSet "inherit_errexit" root - Dash -> True - Sh -> True - Ksh -> False, - hasPipefail = - case shellType params of - Bash -> isOptionSet "pipefail" root - Dash -> True - Sh -> True - Ksh -> isOptionSet "pipefail" root, - shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), - idMap = getTokenMap root, - parentMap = getParentTree root, - variableFlow = getVariableFlow params root, - tokenPositions = asTokenPositions spec, - cfgAnalysis = CF.analyzeControlFlow cfParams root - } - cfParams = CF.CFGParameters { - CF.cfLastpipe = hasLastpipe params, - CF.cfPipefail = hasPipefail params - } + shell = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec + makeParams extraVars = params + where + params = Parameters { + rootNode = root, + shellType = shell, + hasSetE = containsSetE root, + hasLastpipe = + case shellType params of + Bash -> isOptionSet "lastpipe" root + Dash -> False + Sh -> False + Ksh -> True, + hasInheritErrexit = + case shellType params of + Bash -> isOptionSet "inherit_errexit" root + Dash -> True + Sh -> True + Ksh -> False, + hasPipefail = + case shellType params of + Bash -> isOptionSet "pipefail" root + Dash -> True + Sh -> True + Ksh -> isOptionSet "pipefail" root, + shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), + idMap = getTokenMap root, + parentMap = getParentTree root, + variableFlow = getVariableFlow params root, + tokenPositions = asTokenPositions spec, + cfgAnalysis = CF.analyzeControlFlow cfParams root, + additionalKnownVariables = extraVars + } + cfParams = CF.CFGParameters { + CF.cfLastpipe = hasLastpipe params, + CF.cfPipefail = hasPipefail params, + CF.cfAdditionalInitialVariables = additionalKnownVariables params + } root = asScript spec diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index f882adc..32a663f 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -167,7 +167,9 @@ 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 + cfPipefail :: Bool, + -- Additional variables to consider defined + cfAdditionalInitialVariables :: [String] } data CFGResult = CFGResult { diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs index 3b4f957..0d95e86 100644 --- a/src/ShellCheck/CFGAnalysis.hs +++ b/src/ShellCheck/CFGAnalysis.hs @@ -197,12 +197,13 @@ unreachableState = modified newInternalState { } -- The default state we assume we get from the environment -createEnvironmentState :: InternalState -createEnvironmentState = do +createEnvironmentState :: CFGParameters -> InternalState +createEnvironmentState params = do foldl' (flip ($)) newInternalState $ concat [ addVars Data.internalVariables unknownVariableState, addVars Data.variablesWithoutSpaces spacelessVariableState, - addVars Data.specialIntegerVariables integerVariableState + addVars Data.specialIntegerVariables integerVariableState, + addVars (cfAdditionalInitialVariables params) unknownVariableState ] where addVars names val = map (\name -> insertGlobal name val) names @@ -1344,7 +1345,7 @@ analyzeControlFlow params t = runST $ f cfg entry exit where f cfg entry exit = do - let env = createEnvironmentState + let env = createEnvironmentState params ctx <- newCtx $ cfGraph cfg -- Do a dataflow analysis starting on the root node exitState <- runRoot ctx env entry exit diff --git a/src/ShellCheck/Checker.hs b/src/ShellCheck/Checker.hs index c79f90f..513390a 100644 --- a/src/ShellCheck/Checker.hs +++ b/src/ShellCheck/Checker.hs @@ -88,11 +88,12 @@ checkScript sys spec = do asTokenPositions = tokenPositions, asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec } where as = newAnalysisSpec root - let analysisMessages = - maybe [] - (arComments . analyzeScript . analysisSpec) - $ prRoot result + let getAnalysisMessages = + case prRoot result of + Just root -> arComments <$> (analyzeScript sys $ analysisSpec root) + Nothing -> return [] let translator = tokenToPosition tokenPositions + analysisMessages <- getAnalysisMessages return . nub . sortMessages . filter shouldInclude $ (parseMessages ++ map translator analysisMessages) diff --git a/src/ShellCheck/Debug.hs b/src/ShellCheck/Debug.hs index b6015e5..6492e54 100644 --- a/src/ShellCheck/Debug.hs +++ b/src/ShellCheck/Debug.hs @@ -117,7 +117,8 @@ dummySystemInterface = mockedSystemInterface [ cfgParams :: CFGParameters cfgParams = CFGParameters { cfLastpipe = False, - cfPipefail = False + cfPipefail = False, + cfAdditionalInitialVariables = [] } -- An example script to play with diff --git a/src/ShellCheck/Interface.hs b/src/ShellCheck/Interface.hs index 077212f..1329a4d 100644 --- a/src/ShellCheck/Interface.hs +++ b/src/ShellCheck/Interface.hs @@ -87,7 +87,9 @@ data SystemInterface m = SystemInterface { -- find the sourced file siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath, -- | Get the configuration file (name, contents) for a filename - siGetConfig :: String -> m (Maybe (FilePath, String)) + siGetConfig :: String -> m (Maybe (FilePath, String)), + -- | Look up Portage Eclass variables + siGetPortageVariables :: m (Map.Map String [String]) } -- ShellCheck input and output @@ -141,7 +143,8 @@ newSystemInterface = SystemInterface { siReadFile = \_ _ -> return $ Left "Not implemented", siFindSource = \_ _ _ name -> return name, - siGetConfig = \_ -> return Nothing + siGetConfig = \_ -> return Nothing, + siGetPortageVariables = return Map.empty } -- Parser input and output