Skip to content

Pnpm lock refactor#1725

Open
csasarak wants to merge 14 commits into
masterfrom
pnpm-lock-refactor
Open

Pnpm lock refactor#1725
csasarak wants to merge 14 commits into
masterfrom
pnpm-lock-refactor

Conversation

@csasarak

Copy link
Copy Markdown
Contributor

Overview

The PNPM lockfile support is hard to handle because instead of supporting concrete versions of the config we kind of have a bunch of conditionals supporting different aspects of it in an ad-hoc way. This was an attempt to split the Pnpm lock processor into separate pieces that are only active when relevant. I'm hoping that it will make it easier for us to do work on Pnpm and know that the work is more narrowly scoped to one lockfile format.

Acceptance criteria

The Pnpm lockfile code is easier to read/understand and likely to be easier to work with than what we have today.

Testing plan

I'm relying entirely on our test suite. I don't have a super deep knowledge of all the vagaries of different pnpm lockfile formats. I had a look at our tests and the coverage looks fairly decent though I don't know how or if it captures more esoteric features.

Risks

The main risk is I broke something and have not realized. The other is that this isn't really an improvement to anyone but me.

Metrics

References

There isn't a ticket for this. It's just something I've been doing in the background since I find this area of the CLI to be really hard to grok.

Checklist

  • I added tests for this PR's change (or explained in the PR description why tests don't make sense).
  • If this PR introduced a user-visible change, I added documentation into docs/.
  • If this PR added docs, I added links as appropriate to the user manual's ToC in docs/README.ms and gave consideration to how discoverable or not my documentation is.
  • If this change is externally visible, I updated Changelog.md. If this PR did not mark a release, I added my changes into an ## Unreleased section at the top.
  • If I made changes to .fossa.yml or fossa-deps.{json.yml}, I updated docs/references/files/*.schema.json AND I have updated example files used by fossa init command. You may also need to update these if you have added/removed new dependency type (e.g. pip) or analysis target type (e.g. poetry).
  • If I made changes to a subcommand's options, I updated docs/references/subcommands/<subcommand>.md.

csasarak added 13 commits May 19, 2026 11:02
…d helpers

- Extract per-version graph builders (buildGraphV4or5, buildGraphV678, buildGraphV9)
- Replace Bool useLabels with LabelingMode sum type
- Share getPkgNameVersionV6/V9 via parseAtKey
- Extract buildGraphPreV9 to eliminate duplicate bodies
- Lift shared helpers to top level (toDependency, toDep, applyLabels, etc.)
- Fix context string 'Npm Lockfile' -> 'Pnpm Lockfile'
- Remove unused Control.Monad (when) import
@csasarak csasarak requested a review from zlav June 24, 2026 21:13
@csasarak csasarak requested a review from a team as a code owner June 24, 2026 21:13
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds shared pnpm lockfile types and parsing for multiple lockfile versions, introduces version-specific key parsing and graph config modules for pnpm v4/v5, v6/v7/v8, and v9, and refactors PnpmLock to use a config-driven graph builder. It also exposes the new pnpm modules in the Cabal file and updates CLAUDE.md with a Rust test-code exception for partial functions.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: refactoring PNPM lockfile handling.
Description check ✅ Passed The description includes all required sections and provides reasonable detail on intent, testing, risks, references, and checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 27-35: The test-only partial-function allowance in CLAUDE.md is
too broad and should be narrowed to match the actual policy. Update the “Tests”
guidance so it only permits the specific partial functions already exempted in
HLint for test/spec modules, and keep the rest of the style guidance aligned
with proving safety or avoiding partials elsewhere. Use the existing “Tests” and
Rust style sections in CLAUDE.md as the place to tighten the wording.

In `@src/Strategy/Node/Pnpm/PnpmLock.hs`:
- Around line 236-240: The warning branch in the pnpm lockfile version check
uses a match guard on the result of Text.uncons in the LockfileV4Or5 handling,
which violates the Haskell style guideline against guards in normal code.
Refactor the logic in PnpmLock.hs around the lockfileRawVersion check to use a
nested case or a small shared helper that parses the major version, then keep
the same warning behavior in the branch that detects versions starting with 1,
2, or 3.
- Around line 150-156: The lookup order in the package resolution logic is too
permissive: it checks the stripped version before the exact peer-suffixed
version, which can bind edges/envs to the wrong package instance. Update the
resolution in PnpmLock to prefer the exact depVersion first and only fall back
to withoutPeerDepSuffix for both maybeNonRegistrySrcPackage and
maybeRegistrySrcPackage, keeping the existing Map.lookup flow but reversing the
precedence around depVersion and strippedVersion.

In `@src/Strategy/Node/Pnpm/Types.hs`:
- Around line 335-344: The version classifier in classifyVersion only inspects
the first character of the lockfile version, so multi-digit majors like 10 are
misrouted through the v4/v5 path. Update classifyVersion in Types.hs to parse
the full numeric major component from the TextLike value, dispatching valid
majors to the appropriate VersionClassifier and sending any major >= 9 through
the v9 path; if the format is not a valid numeric major, fail explicitly. Remove
the current match-guard-based character checks in this classifier and keep the
logic centered on the parsed major version.
- Around line 242-286: The ProjectMap and PackageData parsers currently ignore
optionalDependencies, so those edges never reach the dependency graph; add
optional-dependency fields to the shared model and parse them in the FromJSON
instances alongside dependencies and devDependencies. Then thread the new data
through the graph-building path used by buildGraphCore so importer- and
package-level optional dependencies are retained in direct and transitive
construction, following the same treatment as production dependencies unless
there is a separate policy.
- Around line 111-115: Replace the match guard in the new Haskell code with a
case expression to comply with the repository rule against guards. Update the
logic in the Pnpm types handling, using the relevant function or pattern match
in Types.hs, so the same branching is expressed with case instead of a guard
while preserving behavior.

In `@src/Strategy/Node/Pnpm/V4_8.hs`:
- Around line 35-46: The parseAtKey function has a layout/parse issue because
the >>= continuation is aligned with the case alternatives instead of being part
of the Just txt branch. Update parseAtKey in the V4_8 strategy so the
Nothing/Just handling is expressed cleanly, with the slashRequired check using
if instead of a guard where needed, and place the bind inside the Just txt
branch using do notation so the parser can read the function correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 344afd3e-573f-4206-88c7-da352808f958

📥 Commits

Reviewing files that changed from the base of the PR and between b5a1c53 and 6414877.

📒 Files selected for processing (6)
  • CLAUDE.md
  • spectrometer.cabal
  • src/Strategy/Node/Pnpm/PnpmLock.hs
  • src/Strategy/Node/Pnpm/Types.hs
  • src/Strategy/Node/Pnpm/V4_8.hs
  • src/Strategy/Node/Pnpm/V9.hs

Comment thread CLAUDE.md
Comment on lines +27 to +35
- **Tests**: Partial functions (`head`, `tail`, `!!`, etc.) are acceptable in test code.

## Code Style Guidelines - Rust
- Format with `rustfmt` via `cargo fmt`
- Lint with Clippy via `cargo clippy`
- Follow standard Rust idioms including error handling
- Use the Rust embedded tools within the Haskell code via the extlib directory

For complete guidelines, see `docs/contributing/STYLE-GUIDE.md` No newline at end of file
For complete guidelines, see `docs/contributing/STYLE-GUIDE.md`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Policy references =="
rg -n --hidden -S 'partial functions|head|tail|!!|error|undefined|test code|Tests' \
  CLAUDE.md docs/contributing/STYLE-GUIDE.md .hlint.yaml hlint.yaml cabal.project package.yaml \
  2>/dev/null || true

echo "== Test-only lint exclusions =="
rg -n --hidden -S 'exclude.*test|test.*exclude|ignore.*test|test.*partial|partial.*test' \
  .hlint.yaml hlint.yaml cabal.project package.yaml *.cabal \
  2>/dev/null || true

Repository: fossas/fossa-cli

Length of output: 2445


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CLAUDE.md context =="
sed -n '20,38p' CLAUDE.md

echo
echo "== HLint partial-function rules =="
sed -n '56,82p' .hlint.yaml

echo
echo "== STYLE-GUIDE partial-function section =="
sed -n '181,194p' docs/contributing/STYLE-GUIDE.md

Repository: fossas/fossa-cli

Length of output: 3463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CLAUDE.md context =="
sed -n '20,38p' CLAUDE.md

echo
echo "== HLint partial-function rules =="
sed -n '56,82p' .hlint.yaml

echo
echo "== STYLE-GUIDE partial-function section =="
sed -n '181,194p' docs/contributing/STYLE-GUIDE.md

Repository: fossas/fossa-cli

Length of output: 3463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CLAUDE.md context =="
sed -n '20,38p' CLAUDE.md

echo
echo "== HLint partial-function rules =="
sed -n '56,82p' .hlint.yaml

echo
echo "== STYLE-GUIDE partial-function section =="
sed -n '181,194p' docs/contributing/STYLE-GUIDE.md

Repository: fossas/fossa-cli

Length of output: 3463


Narrow the test-only partial-function exception. This is broader than the current policy: the HLint config only special-cases a few partial functions in specific **.*Spec/test modules, and the style guide still says to avoid partials and prove safety when they’re necessary. Match CLAUDE.md to that narrower exception.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 27 - 35, The test-only partial-function allowance in
CLAUDE.md is too broad and should be narrowed to match the actual policy. Update
the “Tests” guidance so it only permits the specific partial functions already
exempted in HLint for test/spec modules, and keep the rest of the style guidance
aligned with proving safety or avoiding partials elsewhere. Use the existing
“Tests” and Rust style sections in CLAUDE.md as the place to tighten the
wording.

Comment on lines +150 to +156
let strippedVersion = withoutPeerDepSuffix depVersion
let maybeNonRegistrySrcPackage =
Map.lookup strippedVersion pkgs
<|> Map.lookup depVersion pkgs
let maybeRegistrySrcPackage =
fmap (strippedVersion,) (Map.lookup (mkPkg depName strippedVersion) pkgs)
<|> fmap (depVersion,) (Map.lookup (mkPkg depName depVersion) pkgs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Resolve the exact peer-suffixed package before the stripped fallback.

When both /pkg@1.0.0(peer@x) and /pkg@1.0.0 exist, trying the stripped key first can attach edges/envs to the wrong package instance. Prefer the exact depVersion, then fall back to withoutPeerDepSuffix.

Proposed fix
   let strippedVersion = withoutPeerDepSuffix depVersion
   let maybeNonRegistrySrcPackage =
-        Map.lookup strippedVersion pkgs
-          <|> Map.lookup depVersion pkgs
+        Map.lookup depVersion pkgs
+          <|> Map.lookup strippedVersion pkgs
   let maybeRegistrySrcPackage =
-        fmap (strippedVersion,) (Map.lookup (mkPkg depName strippedVersion) pkgs)
-          <|> fmap (depVersion,) (Map.lookup (mkPkg depName depVersion) pkgs)
+        fmap (depVersion,) (Map.lookup (mkPkg depName depVersion) pkgs)
+          <|> fmap (strippedVersion,) (Map.lookup (mkPkg depName strippedVersion) pkgs)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let strippedVersion = withoutPeerDepSuffix depVersion
let maybeNonRegistrySrcPackage =
Map.lookup strippedVersion pkgs
<|> Map.lookup depVersion pkgs
let maybeRegistrySrcPackage =
fmap (strippedVersion,) (Map.lookup (mkPkg depName strippedVersion) pkgs)
<|> fmap (depVersion,) (Map.lookup (mkPkg depName depVersion) pkgs)
let strippedVersion = withoutPeerDepSuffix depVersion
let maybeNonRegistrySrcPackage =
Map.lookup depVersion pkgs
<|> Map.lookup strippedVersion pkgs
let maybeRegistrySrcPackage =
fmap (depVersion,) (Map.lookup (mkPkg depName depVersion) pkgs)
<|> fmap (strippedVersion,) (Map.lookup (mkPkg depName strippedVersion) pkgs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Strategy/Node/Pnpm/PnpmLock.hs` around lines 150 - 156, The lookup order
in the package resolution logic is too permissive: it checks the stripped
version before the exact peer-suffixed version, which can bind edges/envs to the
wrong package instance. Update the resolution in PnpmLock to prefer the exact
depVersion first and only fall back to withoutPeerDepSuffix for both
maybeNonRegistrySrcPackage and maybeRegistrySrcPackage, keeping the existing
Map.lookup flow but reversing the precedence around depVersion and
strippedVersion.

Comment on lines +236 to +240
LockfileV4Or5 (PnpmLockfileV4Or5 base) ->
case Text.uncons (lockfileRawVersion base) of
Just (c, _)
| c `elem` ['1', '2', '3'] ->
logWarn . pretty $ "pnpm-lock file is using older lockFileVersion: " <> lockfileRawVersion base <> ", which is not officially supported!"

@coderabbitai coderabbitai Bot Jun 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Avoid the match guard in the warning branch.

Use a nested if/case or a shared parsed-major helper here.

As per coding guidelines, Haskell normal code should avoid match guards.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Strategy/Node/Pnpm/PnpmLock.hs` around lines 236 - 240, The warning
branch in the pnpm lockfile version check uses a match guard on the result of
Text.uncons in the LockfileV4Or5 handling, which violates the Haskell style
guideline against guards in normal code. Refactor the logic in PnpmLock.hs
around the lockfileRawVersion check to use a nested case or a small shared
helper that parses the major version, then keep the same warning behavior in the
branch that detects versions starting with 1, 2, or 3.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This match guard is uncomplicated so it can stay.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@csasarak Sounds good — keeping the simple match guard here is reasonable. No further action needed from my side.

Comment on lines +111 to +115
-- > dependencies:
-- > aws-sdk:
-- > specifier: 2.1148.0
-- > version: 2.1148.0
-- @

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace the pattern guard with a case.

This is a small local cleanup to keep the new Haskell code within the repository rule against match guards.

Proposed refactor
 resolveCatalogVersion :: PnpmCatalogs -> Text -> Text -> Text
 resolveCatalogVersion (PnpmCatalogs cats) depName ver
-  | Just catalogName <- Text.stripPrefix "catalog:" ver =
+  = case Text.stripPrefix "catalog:" ver of
+      Just catalogName ->
       let name = if Text.null catalogName then "default" else catalogName
        in fromMaybe ver $ Map.lookup name cats >>= Map.lookup depName
-  | otherwise = ver
+      Nothing -> ver

As per coding guidelines, Haskell normal code should avoid match guards.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- > dependencies:
-- > aws-sdk:
-- > specifier: 2.1148.0
-- > version: 2.1148.0
-- @
resolveCatalogVersion :: PnpmCatalogs -> Text -> Text -> Text
resolveCatalogVersion (PnpmCatalogs cats) depName ver = case Text.stripPrefix "catalog:" ver of
Just catalogName ->
let name = if Text.null catalogName then "default" else catalogName
in fromMaybe ver $ Map.lookup name cats >>= Map.lookup depName
Nothing -> ver
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Strategy/Node/Pnpm/Types.hs` around lines 111 - 115, Replace the match
guard in the new Haskell code with a case expression to comply with the
repository rule against guards. Update the logic in the Pnpm types handling,
using the relevant function or pattern match in Types.hs, so the same branching
is expressed with case instead of a guard while preserving behavior.

Source: Coding guidelines

Comment on lines +242 to +286
data ProjectMap = ProjectMap
{ directDependencies :: Map Text ProjectMapDepMetadata
, directDevDependencies :: Map Text ProjectMapDepMetadata
}
deriving (Show, Eq, Ord)

instance FromJSON ProjectMap where
parseJSON = Yaml.withObject "ProjectMap" $ \obj ->
ProjectMap
<$> obj .:? "dependencies" .!= mempty
<*> obj .:? "devDependencies" .!= mempty

newtype ProjectMapDepMetadata = ProjectMapDepMetadata
{ version :: Text
}
deriving (Show, Eq, Ord)

instance FromJSON ProjectMapDepMetadata where
-- This is v5 lock format
parseJSON (Yaml.String r) = pure $ ProjectMapDepMetadata r
-- This is v6 lock format
parseJSON (Yaml.Object obj) = ProjectMapDepMetadata <$> obj .: "version"
parseJSON other = fail ("Invalid format; expected pure string or an object with a `version` field, got: " <> show other)

--
-- Package data
--

data PackageData = PackageData
{ isDev :: Bool
, name :: Maybe Text -- only provided when non-registry resolver is used
, resolution :: Resolution
, dependencies :: Map Text Text
, peerDependencies :: Map Text Text
}
deriving (Show, Eq, Ord)

instance FromJSON PackageData where
parseJSON = Yaml.withObject "PackageData" $ \obj ->
PackageData
<$> (obj .:? "dev" .!= False)
<*> obj .:? "name"
<*> obj .: "resolution"
<*> (obj .:? "dependencies" .!= mempty)
<*> (obj .:? "peerDependencies" .!= mempty)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Parse optionalDependencies instead of dropping them.

The shared model never reads importer- or package-level optionalDependencies, so those edges are unavailable to buildGraphCore and can be omitted from analysis. Add fields here and wire them through direct/transitive graph construction, likely as production dependencies unless the product has a separate optional-dependency policy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Strategy/Node/Pnpm/Types.hs` around lines 242 - 286, The ProjectMap and
PackageData parsers currently ignore optionalDependencies, so those edges never
reach the dependency graph; add optional-dependency fields to the shared model
and parse them in the FromJSON instances alongside dependencies and
devDependencies. Then thread the new data through the graph-building path used
by buildGraphCore so importer- and package-level optional dependencies are
retained in direct and transitive construction, following the same treatment as
production dependencies unless there is a separate policy.

Comment on lines +335 to +344
classifyVersion :: TextLike -> Parser VersionClassifier
classifyVersion (TextLike ver) = case listToMaybe (toString ver) of
Just '1' -> pure VersionV4Or5
Just '2' -> pure VersionV4Or5
Just '3' -> pure VersionV4Or5
Just '4' -> pure VersionV4Or5
Just '5' -> pure VersionV4Or5
Just x | x `elem` ['6', '7', '8'] -> pure VersionV678
Just '9' -> pure VersionV9
_ -> fail $ "expected numeric lockfileVersion, got: " <> show ver

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Parse the full lockfile major version before dispatching.

listToMaybe only checks the first character, so 10.0 is treated like 1.x and routed through the v4/v5 path. Parse the numeric major component and either route >= 9 to v9 handling or reject it explicitly.

As per coding guidelines, this also removes the match guard in the classifier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Strategy/Node/Pnpm/Types.hs` around lines 335 - 344, The version
classifier in classifyVersion only inspects the first character of the lockfile
version, so multi-digit majors like 10 are misrouted through the v4/v5 path.
Update classifyVersion in Types.hs to parse the full numeric major component
from the TextLike value, dispatching valid majors to the appropriate
VersionClassifier and sending any major >= 9 through the v9 path; if the format
is not a valid numeric major, fail explicitly. Remove the current
match-guard-based character checks in this classifier and keep the logic
centered on the parsed major version.

Source: Coding guidelines

Comment on lines +35 to +46
parseAtKey :: Bool -> Text -> Maybe (Text, Text)
parseAtKey slashRequired pkgKey =
case Text.stripPrefix "/" pkgKey of
Nothing | slashRequired -> Nothing
Nothing -> Just pkgKey
Just txt -> Just txt
>>= \txt -> do
let (nameAndVersion, peerDepInfo) = Text.breakOn "(" txt
let (nameWithSlash, version) = Text.breakOnEnd "@" nameAndVersion
case (Text.stripSuffix "@" nameWithSlash, version) of
(Just name, v) -> Just (name, v <> peerDepInfo)
_ -> Nothing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect the surrounding lines with numbering.
git ls-files 'src/Strategy/Node/Pnpm/V4_8.hs'
echo '---'
wc -l src/Strategy/Node/Pnpm/V4_8.hs
echo '---'
sed -n '1,120p' src/Strategy/Node/Pnpm/V4_8.hs | cat -n

Repository: fossas/fossa-cli

Length of output: 4607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the target file and inspect the surrounding lines with numbering.
git ls-files 'src/Strategy/Node/Pnpm/V4_8.hs'
echo '---'
wc -l src/Strategy/Node/Pnpm/V4_8.hs
echo '---'
sed -n '1,120p' src/Strategy/Node/Pnpm/V4_8.hs | cat -n

Repository: fossas/fossa-cli

Length of output: 4607


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether a Haskell parser/compiler is available to validate the syntax claim.
command -v ghc >/dev/null && ghc --version || true
command -v runghc >/dev/null && runghc --version || true
command -v stack >/dev/null && stack --version || true
command -v cabal >/dev/null && cabal --version || true

Repository: fossas/fossa-cli

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether a Haskell parser/compiler is available to validate the syntax claim.
command -v ghc >/dev/null && ghc --version || true
command -v runghc >/dev/null && runghc --version || true
command -v stack >/dev/null && stack --version || true
command -v cabal >/dev/null && cabal --version || true

Repository: fossas/fossa-cli

Length of output: 154


🌐 Web query:

Haskell layout rule case alternative line break operator aligned with alternative parse error ">>=" after case alternative

💡 Result:

The parse errors you are encountering when using case expressions or operators like >>= across line breaks are due to Haskell's layout rule (or "off-side rule"), which dictates how indentation defines the structure of code [1]. When you omit explicit curly braces {} and semicolons ; in constructs like case, let, where, or do, Haskell uses the indentation of the first token following the keyword to determine the start of a layout block [1]. Key reasons for these errors include: 1. Insufficient Indentation: If a line within a case alternative or a do block is not indented strictly further than the start of the preceding expression, the parser assumes the current block has ended [2][3]. For example, if a case expression is inside a let block, the alternatives must be indented more than the start of the let binding; otherwise, the compiler may interpret the case alternatives as additional, incorrectly formatted bindings for the let statement [3]. 2. Misalignment of Alternatives: Alternatives within a case expression must be aligned at the same indentation level [2]. If an alternative is less indented, it terminates the case block; if it is indented inconsistently, it may be misparsed as a new, invalid entry in an enclosing block [3]. 3. Interaction with Operators: Operators like >>= (often used in do notation) require careful indentation management. If you break a line after an operator, the next line must be indented further than the start of the expression to be considered a continuation of that expression [2]. If it is not, the compiler may see it as the start of a new, separate statement, leading to a parse error [3]. To resolve these errors: * Always ensure that the body of a case or do block is indented more than the keyword that introduces it [3]. * When breaking lines in expressions (including those using >>=), ensure the continuation is indented further than the start of the expression [2]. * Use explicit braces {} and semicolons ; if you need to bypass layout rules or achieve specific formatting that the off-side rule does not naturally support [1][4]. * Check for "ghost" indentation issues by ensuring you are using spaces consistently, not mixing them with tabs [5]. For complex expressions, you can often work around these limitations by using parentheses to group terms, which can change how the parser interprets the indentation requirements [6].

Citations:


🌐 Web query:

Haskell layout rule case alternative line break operator aligned with alternative parse error ">>=" after case alternative

💡 Result:

In Haskell, the "layout rule" (or "off-side rule") automatically inserts braces and semicolons to define blocks of code based on indentation [1][2]. Parse errors related to "case" alternatives and line breaks almost always stem from incorrect indentation levels relative to the start of the layout block [3][4][5]. When you use keywords like "case... of", "let", "where", or "do", Haskell expects the subsequent items (e.g., the alternatives in a case expression) to be indented further to the right than the keyword itself or the surrounding context [1][2]. Key causes for these errors include: 1. Misaligned Alternatives: If a case alternative is indented at the same level or to the left of the start of the case expression (or its preceding let/do block), the compiler terminates the layout block prematurely [1][4][5]. This often leads to confusing parse errors, such as a "parse error on input '->'" or unexpected token errors, because the compiler believes the block has already ended [3][6][7][4]. 2. Let/Do Block Interference: When a "case" expression is nested inside a "let" or "do" block, its indentation must be strictly greater than the indentation of the "let" or "do" expression, or its components, to be parsed as part of that block [3][5]. If the indentation is insufficient, the compiler may incorrectly attempt to parse the "case" as a sibling statement or part of an outer scope, leading to syntax failures [3][5]. 3. Resolution: To fix these errors, ensure that all alternatives within a "case" expression are aligned at the same indentation level, and that this level is greater than the indentation of the "case" keyword itself [4][5]. Alternatively, you can use explicit braces and semicolons (e.g., case x of { A ->...; B ->... }), which completely bypasses the layout rule and is immune to these indentation-related parse errors [8][9][5]. If you are receiving a "parse error on input '>>='" or similar after a case alternative, it often indicates that the previous case branch was not properly terminated, causing the parser to see the next line as an extension of the previous expression rather than a new statement in the "do" or "let" block [6][10]. Ensuring proper indentation alignment is the standard solution [3][4].

Citations:


🌐 Web query:

Haskell layout rule case alternative line break operator aligned with alternative parse error ">>=" after case alternative

💡 Result:

In Haskell, the parse error occurring after a case alternative—often manifesting as a failure on the arrow (->) or subsequent lines—is typically caused by the interaction of the layout rule (the "off-side" rule) with nested syntactic blocks [1][2]. When a case expression is nested inside another block, such as a let or do expression, the parser uses the indentation level of the first lexeme following the keyword (like case, let, or where) to define the boundaries of that block [1][3][2]. If the alternatives of a case expression are not indented sufficiently beyond the indentation level of the enclosing block, the layout rule may incorrectly interpret those alternatives as new, separate items belonging to the parent block rather than as part of the case expression itself [2][4][5]. For example, in the following code, if the case alternatives are at the same indentation level as the variable defined in the let block, the compiler attempts to parse them as additional bindings for that let block, leading to a parse error when it encounters the case syntax (such as ->) instead of a valid binding [2][4]: -- Incorrect: alternatives are not indented enough let x = case y of A -> 1 -- Misinterpreted as part of the let block B -> 2 To resolve this, the alternatives must be indented further to the right than the starting column of the expression they belong to [1][4]. -- Correct: alternatives indented beyond the let binding start let x = case y of A -> 1 B -> 2 The Haskell layout rule is technically specified in terms of inserting implicit curly braces and semicolons [6][7]. When the compiler encounters an "invalid" token at a point where a closing brace could legally be inserted, it invokes the parse-error rule to potentially close the current layout context [8][3]. If your indentation is ambiguous, the parser may prematurely close the case block or fail to open one, resulting in the observed parse errors [2][4]. Always ensure that content inside a case block is clearly indented relative to the keyword or the start of the expression to avoid these layout ambiguities [1][2].

Citations:


Fix the case/>>= layout in parseAtKey.
>>= is aligned with the case alternatives instead of the Just txt branch, so this is a parse error. Fold the bind into the branch with do notation; the guard should become if.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Strategy/Node/Pnpm/V4_8.hs` around lines 35 - 46, The parseAtKey function
has a layout/parse issue because the >>= continuation is aligned with the case
alternatives instead of being part of the Just txt branch. Update parseAtKey in
the V4_8 strategy so the Nothing/Just handling is expressed cleanly, with the
slashRequired check using if instead of a guard where needed, and place the bind
inside the Just txt branch using do notation so the parser can read the function
correctly.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant