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:
Russell Harmon 2016-06-18 22:15:01 -07:00 committed by Ng Zhi An
parent b5e5d249c4
commit 4470fe715c
3 changed files with 59 additions and 17 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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
} }