packages feed

nova-nix-0.8.0.0: src/Nix/Eval/AttrPath.hs

-- | Attribute-path selection, the machinery behind @build -A@.
--
-- An attribute path is a dotted sequence of names selected left to right
-- from an evaluated value: @stdenv.mkDerivation@ takes @mkDerivation@ out
-- of @stdenv@.  A component may be double-quoted to carry a literal dot,
-- as in @foo.\"bar.baz\"@.
--
-- Upstream tokenizes the same way (@parseAttrPath@,
-- src\/libexpr\/attr-path.cc), including three behaviours that read as
-- accidents but are load-bearing for compatibility: quotes concatenate
-- rather than delimit, so @foo\"bar\"@ is the single name @foobar@; the
-- final component is pushed only when non-empty, so a trailing dot and a
-- lone @\"\"@ both vanish instead of becoming a component; and an interior
-- empty component (a leading or doubled dot) survives tokenizing to be
-- rejected during selection, after the type of the value it would index.
--
-- Two upstream behaviours are deliberately absent.  A numeric component
-- indexes a list upstream (@-A foo.3.bar@); here it is an ordinary name,
-- because nothing this selects over is a list and upstream's
-- integer-or-name test defers to a C++ numeric parse whose accepted
-- spellings (a leading @+@, leading zeros) have not been checked against a
-- running Nix.  And a near-miss attribute name gets no @Did you mean@
-- line, which upstream renders from a Levenshtein search this evaluator
-- has no suggestion channel to carry.
--
-- The four messages otherwise match upstream byte for byte, diffed against
-- @nix-instantiate@ 2.33.2.
module Nix.Eval.AttrPath
  ( parseAttrPath,
    selectAttrPath,
  )
where

import Data.Text (Text)
import qualified Data.Text as T
import Nix.Eval (MonadEval, NixValue (..), attrSetLookup, force)

-- ---------------------------------------------------------------------------
-- Tokenizing
-- ---------------------------------------------------------------------------

-- | Component separator.
attrSeparator :: Char
attrSeparator = '.'

-- | Opens and closes a component that may contain 'attrSeparator'.
quoteChar :: Char
quoteChar = '"'

quoteMark :: Text
quoteMark = T.singleton quoteChar

isDelimiter :: Char -> Bool
isDelimiter c = c == attrSeparator || c == quoteChar

-- | Split an attribute path into its components.
--
-- @Left@ carries a message ready for the user; the only way to get one is
-- a quote that is never closed.  An empty component is returned rather
-- than rejected, because upstream reports it against the value being
-- indexed and so cannot decide it here.
parseAttrPath :: Text -> Either Text [Text]
parseAttrPath path = go path T.empty []
  where
    go rest !current acc =
      let (plain, delimited) = T.break isDelimiter rest
          taken = current <> plain
       in case T.uncons delimited of
            Nothing
              | T.null taken -> Right (reverse acc)
              | otherwise -> Right (reverse (taken : acc))
            Just (c, more)
              | c == attrSeparator -> go more T.empty (taken : acc)
              | otherwise ->
                  let (quotedRun, closing) = T.breakOn quoteMark more
                   in case T.stripPrefix quoteMark closing of
                        Nothing -> Left (missingQuoteMessage path)
                        Just after -> go after (taken <> quotedRun) acc

-- ---------------------------------------------------------------------------
-- Selecting
-- ---------------------------------------------------------------------------

-- | Follow an attribute path into an evaluated value, forcing each step.
--
-- Only the components named are forced: selecting one attribute leaves its
-- siblings as thunks, so naming one package does not evaluate the rest of
-- a package set.
selectAttrPath :: (MonadEval m) => Text -> NixValue -> m (Either Text NixValue)
selectAttrPath path root = case parseAttrPath path of
  Left err -> pure (Left err)
  Right names -> walk names root
  where
    walk [] val = pure (Right val)
    walk (name : rest) val = case val of
      VAttrs attrs
        | T.null name -> pure (Left (emptyNameMessage path))
        | otherwise -> case attrSetLookup name attrs of
            Nothing -> pure (Left (notFoundMessage name path))
            Just thunk -> force thunk >>= walk rest
      -- Ordered as upstream orders it: the type of what is being indexed
      -- is reported before an empty component is complained about.
      _ -> pure (Left (notASetMessage path (describe val)))

-- | How a value is named in a selection error.
--
-- Deliberately not 'typeOfValue', which answers @builtins.typeOf@ and so
-- says @int@ and @lambda@.  These are upstream's @showType@ words, article
-- included, so the message reads as upstream's does; @null@ is the one
-- that takes no article.  Verified against @nix-instantiate@ 2.33.2.
describe :: NixValue -> Text
describe val = case val of
  VInt _ -> "an integer"
  VFloat _ -> "a float"
  VBool _ -> "a Boolean"
  VNull -> "null"
  VStr _ _ -> "a string"
  VPath _ -> "a path"
  VList _ -> "a list"
  VAttrs _ -> "a set"
  VDerivation _ -> "a set"
  VLambda {} -> "a function"
  VBuiltin _ _ -> "a function"
  VCompiledRegex _ -> "a function"

-- ---------------------------------------------------------------------------
-- Messages
-- ---------------------------------------------------------------------------

missingQuoteMessage :: Text -> Text
missingQuoteMessage path =
  "missing closing quote in selection path '" <> path <> "'"

emptyNameMessage :: Text -> Text
emptyNameMessage path =
  "empty attribute name in selection path '" <> path <> "'"

notFoundMessage :: Text -> Text -> Text
notFoundMessage name path =
  "attribute '" <> name <> "' in selection path '" <> path <> "' not found"

notASetMessage :: Text -> Text -> Text
notASetMessage path actual =
  "the expression selected by the selection path '"
    <> path
    <> "' should be a set but is "
    <> actual