Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Correctly parse v3 lockfiles #1793

Merged
merged 4 commits into from
Jun 6, 2022

Conversation

cristiano-belloni
Copy link
Contributor

Problem

  • @yarnpkg/lockfile is able to parse lockfile v1 format correctly but not lockfile v3 format (it throws immediately)
  • The tools based on @yarnpkg/lockfile that we were using error with the lockfile v3 format
  • It seems that lockfiles v2+ are not supported because they are valid yaml

Solution

  • Try to parse lockfile directly with @yarnpkg/lockfile
  • If it crashes, parse it manually from YAML.

@changeset-bot
Copy link

changeset-bot bot commented Jun 1, 2022

🦋 Changeset detected

Latest commit: 35ca64c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
modular-scripts Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls
Copy link
Collaborator

coveralls commented Jun 1, 2022

Coverage Status

Coverage decreased (-0.1%) to 26.667% when pulling 35ca64c on fix/parse-lockfile-manually into 2c36b08 on main.

@cristiano-belloni cristiano-belloni changed the title Correctly parse v3 lockfiles Fix: Correctly parse v3 lockfiles Jun 1, 2022
return false;
});
});
// Prepare the parsed lockfile for access
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment seems to be floating away from what it describes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

);
let lockDeps: Dependency = {};

try {
Copy link
Contributor

Choose a reason for hiding this comment

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

The getPackageDependencies function is very long. You could begin to break it down by moving each of these v1 and v2+ operations to separate functions.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also recommend splitting these into separate functions - it's unclear to the reader which thing might fail and therefore why the catch block is larger than the try block... consider:

function parseYarnLock(lockFile: string, deps: Dependency): Dependency {
  return parseYarnLockV1(lockFile, deps) || parseYarnLockV3(lockFile, deps);
}

function parseYarnLockV1(lockFile: string, deps: Dependency): Dependency | null {
  try {
    const parsedLockfile = lockfile.parse(lockFile);
    return Object.entries(deps).reduce(...);
  }
  catch (e) { return null }
}

function parseYarnLockV3(lockFile: string, deps: Dependency): Dependency {
  return Object.entries(yaml.load(lockFile) as LockFileEntries).reduce(...);
}

These could then be more readily unit/integration tested in isolation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// Parse: yarn v3 lock comes with entries like "'yargs@npm:^15.0.2, yargs@npm:^15.1.0, yargs@npm:^15.3.1, yargs@npm:^15.4.1'"
Object.entries(parsedLockfile).forEach(([name, { version }]) => {
const entryDependencies = name.split(', ');
dependencyArray.some(([dependencyName, dependencyVersion]) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

As you're not actually using the return value of Array.prototype.some, a for loop with a break statement would be clearer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I find it quite idiomatic when we ignore the return value (functional breaking loop), but no prob, done.

} catch (e) {
// Try to parse as yaml (v2+) - https://github.com/yarnpkg/yarn/issues/5629#issuecomment-753418765
const parsedLockfile = yaml.load(lockFile) as LockFileEntries;
const dependencyArray = Object.entries(deps);
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between dependencyArray and Object.entries(parsedLockfile)? I don't quite understand what this O(n^2) algo is doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

dependencyArray is the array of dependencies as extracted from source (likely very small), while Object.entries(parsedLockfile) is the array of entries from the lockfile (possibly big). It's quadratic but not as in O(n^2), more like O(n*m), where m << n (greatly less than).

We are traversing all the lockfile entries once and, for every entry, split the key in all the dependencies it represents, then match each with the dependencies extracted from source.

Copy link
Contributor

@benpryke benpryke Jun 2, 2022

Choose a reason for hiding this comment

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

Thanks for the explanation. Why do we not need to add the dependencies to the right of the parsedLockfile entries after a match is found?

When you extract this to a separate function, I'd recommend adding a little context so it's easier for the next person who reads this to get to grips with the why. I'm not familiar with the new lockfile format!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored + added additional comments.

@@ -120,3 +103,46 @@ export async function getPackageDependencies(
);
return { manifest, resolutions };
}

function parseYarnLock(lockFile: string, deps: Dependency): Dependency {
return parseYarnLockV1(lockFile, deps) || parseYarnLockV3(lockFile, deps);
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we are explicitly catering to yarn 1 and yarn 3, can we add this to isYarnInstalled in startupCheck.ts ?

Copy link
Contributor

Choose a reason for hiding this comment

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

By this, I mean the check for the version of yarn to be explicit with our compatibility

Copy link
Contributor

Choose a reason for hiding this comment

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

Spoke offline and decided to create a separate ticket to resolve yarn version compatibility

@cristiano-belloni cristiano-belloni merged commit 89ca8a7 into main Jun 6, 2022
@cristiano-belloni cristiano-belloni deleted the fix/parse-lockfile-manually branch June 6, 2022 12:54
@github-actions github-actions bot mentioned this pull request Jun 6, 2022
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.

5 participants