Support emitting a correct end column on SC2086
This does the necessary work to emit end columns on AST analyses. SC2086 is made to emit a correct end column as an illustrative example. For example: ``` $ shellcheck -s bash -f json /dev/stdin <<< 'echo $1' [{"file":"/dev/stdin","line":1,"endLine":1,"column":6,"endColumn":8,"level":"info","code":2086,"message":"Double quote to prevent globbing and word splitting."}] ``` This change deprecates the parser's getNextId and getNextIdAt, replacing it with a new withNextId function. This function has the type signature: withNextId :: Monad m => ParsecT s UserState (SCBase m) (Id -> b) -> ParsecT s UserState (SCBase m) b Specifically, it should be used to wrap read* functions and will pass in a newly generated Id which should be used to represent that node. Sub-parsers will need their own call to withNextId in order to get a unique Id. In doing this, withNextId can now track both the entry and exit position of every read* parser which uses it, enabling the tracking of end columns throughout the application.
This commit is contained in:
parent
b5e5d249c4
commit
4470fe715c
|
@ -29,6 +29,7 @@ import Data.Functor
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Ord
|
import Data.Ord
|
||||||
|
import Control.Applicative
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified System.IO
|
import qualified System.IO
|
||||||
|
@ -37,11 +38,14 @@ import Control.Monad
|
||||||
|
|
||||||
import Test.QuickCheck.All
|
import Test.QuickCheck.All
|
||||||
|
|
||||||
tokenToPosition map (TokenComment id c) = fromMaybe fail $ do
|
tokenToPosition startMap endMap (TokenComment id c) = fromMaybe fail $ do
|
||||||
position <- Map.lookup id map
|
position <- maybePosition
|
||||||
return $ PositionedComment position position c
|
endPosition <- maybeEndPosition <|> maybePosition
|
||||||
|
return $ PositionedComment position endPosition c
|
||||||
where
|
where
|
||||||
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
fail = error "Internal shellcheck error: id doesn't exist. Please report!"
|
||||||
|
maybeEndPosition = Map.lookup id endMap
|
||||||
|
maybePosition = Map.lookup id startMap
|
||||||
|
|
||||||
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
|
||||||
checkScript sys spec = do
|
checkScript sys spec = do
|
||||||
|
@ -62,7 +66,7 @@ checkScript sys spec = do
|
||||||
fromMaybe [] $
|
fromMaybe [] $
|
||||||
(arComments . analyzeScript . analysisSpec)
|
(arComments . analyzeScript . analysisSpec)
|
||||||
<$> prRoot result
|
<$> prRoot result
|
||||||
let translator = tokenToPosition (prTokenPositions result)
|
let translator = tokenToPosition (prTokenPositions result) (prTokenEndPositions result)
|
||||||
return . nub . sortMessages . filter shouldInclude $
|
return . nub . sortMessages . filter shouldInclude $
|
||||||
(parseMessages ++ map translator analysisMessages)
|
(parseMessages ++ map translator analysisMessages)
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ data ParseSpec = ParseSpec {
|
||||||
data ParseResult = ParseResult {
|
data ParseResult = ParseResult {
|
||||||
prComments :: [PositionedComment],
|
prComments :: [PositionedComment],
|
||||||
prTokenPositions :: Map.Map Id Position,
|
prTokenPositions :: Map.Map Id Position,
|
||||||
|
prTokenEndPositions :: Map.Map Id Position,
|
||||||
prRoot :: Maybe Token
|
prRoot :: Maybe Token
|
||||||
} deriving (Show, Eq)
|
} deriving (Show, Eq)
|
||||||
|
|
||||||
|
|
|
@ -136,6 +136,38 @@ almostSpace =
|
||||||
char c
|
char c
|
||||||
return ' '
|
return ' '
|
||||||
|
|
||||||
|
withNextId :: Monad m => ParsecT s UserState (SCBase m) (Id -> b) -> ParsecT s UserState (SCBase m) b
|
||||||
|
withNextId p = do
|
||||||
|
start <- getPosition
|
||||||
|
id <- createId
|
||||||
|
setStartPos id start
|
||||||
|
fn <- p
|
||||||
|
let t = fn id
|
||||||
|
end <- getPosition
|
||||||
|
setEndPos id end
|
||||||
|
return t
|
||||||
|
where
|
||||||
|
createId = do
|
||||||
|
state <- getState
|
||||||
|
let id = incId (lastId state)
|
||||||
|
putState $ state {
|
||||||
|
lastId = id
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
where incId (Id n) = Id $ n+1
|
||||||
|
setStartPos id sourcepos = do
|
||||||
|
state <- getState
|
||||||
|
let newMap = Map.insert id sourcepos (positionMap state)
|
||||||
|
putState $ state {
|
||||||
|
positionMap = newMap
|
||||||
|
}
|
||||||
|
setEndPos id sourcepos = do
|
||||||
|
state <- getState
|
||||||
|
let newMap = Map.insert id sourcepos (positionEndMap state)
|
||||||
|
putState $ state {
|
||||||
|
positionEndMap = newMap
|
||||||
|
}
|
||||||
|
|
||||||
--------- Message/position annotation on top of user state
|
--------- Message/position annotation on top of user state
|
||||||
data Note = Note Id Severity Code String deriving (Show, Eq)
|
data Note = Note Id Severity Code String deriving (Show, Eq)
|
||||||
data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq)
|
data ParseNote = ParseNote SourcePos SourcePos Severity Code String deriving (Show, Eq)
|
||||||
|
@ -152,6 +184,7 @@ data HereDocContext =
|
||||||
data UserState = UserState {
|
data UserState = UserState {
|
||||||
lastId :: Id,
|
lastId :: Id,
|
||||||
positionMap :: Map.Map Id SourcePos,
|
positionMap :: Map.Map Id SourcePos,
|
||||||
|
positionEndMap :: Map.Map Id SourcePos,
|
||||||
parseNotes :: [ParseNote],
|
parseNotes :: [ParseNote],
|
||||||
hereDocMap :: Map.Map Id [Token],
|
hereDocMap :: Map.Map Id [Token],
|
||||||
pendingHereDocs :: [HereDocContext]
|
pendingHereDocs :: [HereDocContext]
|
||||||
|
@ -159,6 +192,7 @@ data UserState = UserState {
|
||||||
initialUserState = UserState {
|
initialUserState = UserState {
|
||||||
lastId = Id $ -1,
|
lastId = Id $ -1,
|
||||||
positionMap = Map.empty,
|
positionMap = Map.empty,
|
||||||
|
positionEndMap = Map.empty,
|
||||||
parseNotes = [],
|
parseNotes = [],
|
||||||
hereDocMap = Map.empty,
|
hereDocMap = Map.empty,
|
||||||
pendingHereDocs = []
|
pendingHereDocs = []
|
||||||
|
@ -172,6 +206,7 @@ noteToParseNote map (Note id severity code message) =
|
||||||
|
|
||||||
getLastId = lastId <$> getState
|
getLastId = lastId <$> getState
|
||||||
|
|
||||||
|
-- Deprecated by withNextId
|
||||||
getNextIdAt sourcepos = do
|
getNextIdAt sourcepos = do
|
||||||
state <- getState
|
state <- getState
|
||||||
let newId = incId (lastId state)
|
let newId = incId (lastId state)
|
||||||
|
@ -183,6 +218,7 @@ getNextIdAt sourcepos = do
|
||||||
return newId
|
return newId
|
||||||
where incId (Id n) = Id $ n+1
|
where incId (Id n) = Id $ n+1
|
||||||
|
|
||||||
|
-- Deprecated by withNextId
|
||||||
getNextId :: Monad m => SCParser m Id
|
getNextId :: Monad m => SCParser m Id
|
||||||
getNextId = do
|
getNextId = do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
|
@ -1190,8 +1226,7 @@ prop_readDoubleQuoted7 = isOk readSimpleCommand "echo \"${ ls;}bar\""
|
||||||
prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""
|
prop_readDoubleQuoted8 = isWarning readDoubleQuoted "\"\x201Chello\x201D\""
|
||||||
prop_readDoubleQuoted9 = isWarning readDoubleQuoted "\"foo\\n\""
|
prop_readDoubleQuoted9 = isWarning readDoubleQuoted "\"foo\\n\""
|
||||||
prop_readDoubleQuoted10 = isOk readDoubleQuoted "\"foo\\\\n\""
|
prop_readDoubleQuoted10 = isOk readDoubleQuoted "\"foo\\\\n\""
|
||||||
readDoubleQuoted = called "double quoted string" $ do
|
readDoubleQuoted = called "double quoted string" $ withNextId $ do
|
||||||
id <- getNextId
|
|
||||||
startPos <- getPosition
|
startPos <- getPosition
|
||||||
doubleQuote
|
doubleQuote
|
||||||
x <- many doubleQuotedPart
|
x <- many doubleQuotedPart
|
||||||
|
@ -1201,7 +1236,7 @@ readDoubleQuoted = called "double quoted string" $ do
|
||||||
try . lookAhead $ suspectCharAfterQuotes <|> oneOf "$\""
|
try . lookAhead $ suspectCharAfterQuotes <|> oneOf "$\""
|
||||||
when (any hasLineFeed x && not (startsWithLineFeed x)) $
|
when (any hasLineFeed x && not (startsWithLineFeed x)) $
|
||||||
suggestForgotClosingQuote startPos endPos "double quoted string"
|
suggestForgotClosingQuote startPos endPos "double quoted string"
|
||||||
return $ T_DoubleQuoted id x
|
return $ \id -> T_DoubleQuoted id x
|
||||||
where
|
where
|
||||||
startsWithLineFeed (T_Literal _ ('\n':_):_) = True
|
startsWithLineFeed (T_Literal _ ('\n':_):_) = True
|
||||||
startsWithLineFeed _ = False
|
startsWithLineFeed _ = False
|
||||||
|
@ -1544,16 +1579,18 @@ prop_readDollarVariable3 = isWarning (readDollarVariable >> anyChar) "$10"
|
||||||
prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]"
|
prop_readDollarVariable4 = isWarning (readDollarVariable >> string "[@]") "$arr[@]"
|
||||||
prop_readDollarVariable5 = isWarning (readDollarVariable >> string "[f") "$arr[f"
|
prop_readDollarVariable5 = isWarning (readDollarVariable >> string "[f") "$arr[f"
|
||||||
|
|
||||||
readDollarVariable = do
|
readDollarVariable :: Monad m => ParsecT String UserState (SCBase m) Token
|
||||||
id <- getNextId
|
readDollarVariable = withNextId $ do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
|
|
||||||
let singleCharred p = do
|
let
|
||||||
|
singleCharred p = do
|
||||||
n <- p
|
n <- p
|
||||||
value <- wrap [n]
|
value <- wrap [n]
|
||||||
return (T_DollarBraced id value)
|
return $ \id -> (T_DollarBraced id value)
|
||||||
|
|
||||||
let positional = do
|
let
|
||||||
|
positional = do
|
||||||
value <- singleCharred digit
|
value <- singleCharred digit
|
||||||
return value `attempting` do
|
return value `attempting` do
|
||||||
lookAhead digit
|
lookAhead digit
|
||||||
|
@ -1564,17 +1601,15 @@ readDollarVariable = do
|
||||||
let regular = do
|
let regular = do
|
||||||
name <- readVariableName
|
name <- readVariableName
|
||||||
value <- wrap name
|
value <- wrap name
|
||||||
return (T_DollarBraced id value) `attempting` do
|
return (\id -> (T_DollarBraced id value)) `attempting` do
|
||||||
lookAhead $ char '['
|
lookAhead $ char '['
|
||||||
parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)."
|
parseNoteAt pos ErrorC 1087 "Use braces when expanding arrays, e.g. ${array[idx]} (or ${var}[.. to quiet)."
|
||||||
|
|
||||||
try $ char '$' >> (positional <|> special <|> regular)
|
try $ char '$' >> (positional <|> special <|> regular)
|
||||||
|
|
||||||
where
|
where
|
||||||
wrap s = do
|
wrap s = withNextId $ withNextId $ do
|
||||||
x <- getNextId
|
return $ \x y -> T_NormalWord x [T_Literal y s]
|
||||||
y <- getNextId
|
|
||||||
return $ T_NormalWord x [T_Literal y s]
|
|
||||||
|
|
||||||
readVariableName = do
|
readVariableName = do
|
||||||
f <- variableStart
|
f <- variableStart
|
||||||
|
@ -3002,6 +3037,7 @@ parseShell env name contents = do
|
||||||
return ParseResult {
|
return ParseResult {
|
||||||
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
|
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
|
||||||
prTokenPositions = Map.map posToPos (positionMap userstate),
|
prTokenPositions = Map.map posToPos (positionMap userstate),
|
||||||
|
prTokenEndPositions = Map.map posToPos (positionEndMap userstate),
|
||||||
prRoot = Just $
|
prRoot = Just $
|
||||||
reattachHereDocs script (hereDocMap userstate)
|
reattachHereDocs script (hereDocMap userstate)
|
||||||
}
|
}
|
||||||
|
@ -3013,6 +3049,7 @@ parseShell env name contents = do
|
||||||
++ [makeErrorFor err]
|
++ [makeErrorFor err]
|
||||||
++ parseProblems state,
|
++ parseProblems state,
|
||||||
prTokenPositions = Map.empty,
|
prTokenPositions = Map.empty,
|
||||||
|
prTokenEndPositions = Map.empty,
|
||||||
prRoot = Nothing
|
prRoot = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue