Skip to content

feat: add workspace support for packages check licenses#1540

Open
realmeylisdev wants to merge 19 commits into
VeryGoodOpenSource:mainfrom
realmeylisdev:feat/workspace-license-support
Open

feat: add workspace support for packages check licenses#1540
realmeylisdev wants to merge 19 commits into
VeryGoodOpenSource:mainfrom
realmeylisdev:feat/workspace-license-support

Conversation

@realmeylisdev
Copy link
Copy Markdown
Contributor

Summary

This PR adds workspace support to the packages check licenses command, enabling license checking in monorepo projects that use Dart's pub workspace feature.

Reopens #1444 (branch was restored per request).
Closes #1273

Changes

New Files

  • lib/src/pubspec/pubspec.dart - Pubspec parser for detecting workspace configurations
  • test/src/pubspec/pubspec_test.dart - 19 tests for the Pubspec parser

Modified Files

  • lib/src/commands/packages/commands/check/commands/licenses.dart - Added workspace detection and dependency collection
  • test/src/commands/packages/commands/check/commands/licenses_test.dart - Added 3 workspace support tests

How It Works

  1. Auto-detection: When pubspec.yaml contains a workspace property, it's detected as a workspace root
  2. Dependency collection: Dependencies are collected from all workspace members' pubspec.yaml files
  3. Glob pattern support: Workspace paths like packages/* are supported (Dart 3.11+)
  4. Nested workspaces: Recursively handles nested workspaces
  5. Backwards compatible: Non-workspace projects work exactly as before

Test plan

  • All existing license tests pass (42 tests)
  • New workspace support tests pass (3 tests)
  • New Pubspec parser tests pass (19 tests)
  • Static analysis passes with no issues

realmeylisdev and others added 7 commits December 30, 2025 00:24
When a pubspec.yaml declares a workspace property, the command now
recursively collects dependencies from all workspace members and
checks their licenses using the root pubspec.lock.

This enables license checking in monorepo projects that use Dart's
pub workspace feature.

Closes VeryGoodOpenSource#1273
@samitsv
Copy link
Copy Markdown

samitsv commented Mar 18, 2026

@realmeylisdev seems like there are some conflicts that needs to be resolved

Keep both workspace license support functions and ReporterOutputFormat
enum from main.
@realmeylisdev
Copy link
Copy Markdown
Contributor Author

now done.

@samitsv
Copy link
Copy Markdown

samitsv commented Mar 18, 2026

@marcossevilla could we get this PR reviewed which was de-prioritized last time ? #1444 (comment)

@marcossevilla
Copy link
Copy Markdown
Member

@marcossevilla could we get this PR reviewed which was de-prioritized last time ? #1444 (comment)

hi @samitsv, I'll take a look during this week, thanks for pushing this!

also mentioning @brianegan as he was having this issue too, a review or test to check if it's fixing your issue is also appreciated 👍

@samitsv
Copy link
Copy Markdown

samitsv commented Apr 1, 2026

any update on this @marcossevilla

@samitsv
Copy link
Copy Markdown

samitsv commented Apr 8, 2026

any update on this @marcossevilla . Would be great to have this merged soon.

@marcossevilla
Copy link
Copy Markdown
Member

hey @samitsv, we're currently trying the branch against internal repos to verify the changes, I'll leave an update once we finish testing

@brianegan
Copy link
Copy Markdown

brianegan commented Apr 9, 2026

@marcossevilla Functional testing done on my end! PR worked well for a workspace monorepo. Sorry for the delay here -- deadlines.

Comment on lines +36 to +41
@visibleForTesting
const pubspecLockBasename = 'pubspec.lock';

/// The basename of the pubspec file.
@visibleForTesting
const pubspecBasename = 'pubspec.yaml';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if we have a library for pubspecs, maybe it's more natural for these variables to part of it

Comment on lines +576 to +586
/// Attempts to parse a [Pubspec] from a file.
///
/// Returns `null` if the file doesn't exist or cannot be parsed.
Pubspec? _tryParsePubspec(File pubspecFile) {
if (!pubspecFile.existsSync()) return null;
try {
return Pubspec.fromFile(pubspecFile);
} on PubspecParseException catch (_) {
return null;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same here, maybe it's better if it's part of pubspec library

Comment thread lib/src/pubspec/pubspec.dart Outdated
Comment on lines +54 to +58
} on TypeError catch (_) {
throw const PubspecParseException('Failed to parse YAML content');
} on YamlException catch (_) {
throw const PubspecParseException('Failed to parse YAML content');
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
} on TypeError catch (_) {
throw const PubspecParseException('Failed to parse YAML content');
} on YamlException catch (_) {
throw const PubspecParseException('Failed to parse YAML content');
}
} on Exception catch (_) {
throw const PubspecParseException('Failed to parse YAML content');
}

since it's the same exception being thrown

Comment thread test/src/pubspec/pubspec_test.dart Outdated
import 'package:very_good_cli/src/pubspec/pubspec.dart';

void main() {
group('$Pubspec', () {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
group('$Pubspec', () {
group(Pubspec, () {

Comment thread test/src/pubspec/pubspec_test.dart Outdated
});
});

group('$PubspecParseException', () {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
group('$PubspecParseException', () {
group(PubspecParseException, () {

Comment thread test/src/pubspec/pubspec_test.dart Outdated
});
});

group('$PubspecResolution', () {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
group('$PubspecResolution', () {
group(PubspecResolution, () {

Address review feedback on PR VeryGoodOpenSource#1540:
- Move `pubspecBasename` constant from licenses.dart to pubspec library
- Replace private `_tryParsePubspec` helper with `Pubspec.tryParse` static
- Simplify `Pubspec.fromString` by replacing the throwing `as` cast with
  an `is!` check, removing the `TypeError` catch and lint suppression
- Pass class types directly to `group()` in pubspec_test.dart
@marcossevilla
Copy link
Copy Markdown
Member

@realmeylisdev CI is failing, also found we could just use https://pub.dev/packages/pubspec_parse instead of creating a pubspec library

@realmeylisdev
Copy link
Copy Markdown
Contributor Author

CI Failure Analysis

The e2e (docs_site) failure is unrelated to this PR's changes. The e2e (flutter_plugin) cancellation is a cascade from fail-fast: true in the matrix strategy.

Root Cause

The docs_site e2e test generates a Docusaurus project and runs npm run build. The build fails because webpack's ProgressPlugin API dropped support for the name option:

ValidationError: Invalid options object. Progress Plugin has been initialized
using an options object that does not match the API schema.
 - options has an unknown property 'name'.

This is an upstream Docusaurus/webpack incompatibility in the very_good_docs_site template — the fix belongs in very_good_templates, not here.

PR Code Verification

  • All 73 unit tests pass (pubspec parser + licenses command including workspace support)
  • dart analyze --fatal-infos --fatal-warnings — clean
  • dart format — clean
  • All other CI checks (build on ubuntu/windows, all other e2e tests, pana, spell-check, link-check) pass

@marcossevilla
Copy link
Copy Markdown
Member

hey @realmeylisdev, we need to use package:pubspec_parse still

@marcossevilla
Copy link
Copy Markdown
Member

hey @realmeylisdev, are you able to work on the changes? otherwise we could close the PR and the team will tackle this issue separately

Replaces the hand-rolled Pubspec parser with `package:pubspec_parse`,
which is already a direct dependency. Keeps only the workspace
helpers (`tryParsePubspec`, `PubspecWorkspace` extension,
`resolveWorkspaceMembers`) that pubspec_parse does not provide.

Addresses review feedback on VeryGoodOpenSource#1540.
@realmeylisdev
Copy link
Copy Markdown
Contributor Author

Hey @marcossevilla — swapped the custom parser for package:pubspec_parse per review in 8ca2652. The new lib/src/pubspec/pubspec.dart is down to ~85 lines and only keeps the workspace glob/filesystem helpers that pubspec_parse does not provide (tryParsePubspec, PubspecWorkspace extension with isWorkspaceRoot/isWorkspaceMember, and resolveWorkspaceMembers). Net diff: −238 lines. Coverage is preserved at 100% on both changed files (pubspec.dart 29/29, licenses.dart 240/240).

The two failing e2e checks (docs_site, flutter_plugin) are identical to the failures already present on main — see run https://github.com/VeryGoodOpenSource/very_good_cli/actions/runs/24140371198 (from #1567 merge, 4 days ago). Both are template-repo issues (Docusaurus/webpack ProgressPlugin incompatibility and mocktail when/verify inference warnings respectively), not related to this PR.

Ready for another look.

@samitsv
Copy link
Copy Markdown

samitsv commented Apr 23, 2026

Can we please get this reviewed :) @marcossevilla

@marcossevilla
Copy link
Copy Markdown
Member

@realmeylisdev CI should be fixed, you can update your branch to get the latest changes

@realmeylisdev
Copy link
Copy Markdown
Contributor Author

@realmeylisdev CI should be fixed, you can update your branch to get the latest changes

done.

@marcossevilla

This comment was marked as outdated.

@very-good-code-bot

This comment was marked as outdated.

@erickzanardo
Copy link
Copy Markdown
Member

@vgvbot perform a code review focused on verifying nits and code readability

@very-good-code-bot
Copy link
Copy Markdown
Contributor

very-good-code-bot Bot commented May 5, 2026

I'll review the code and share my feedback shortly.

  • Preparing environment
    • Cloning repository
    • Setting up dev environment
  • Reviewing code
  • Posting review

Code review submitted.

Copy link
Copy Markdown
Contributor

@very-good-code-bot very-good-code-bot Bot left a comment

Choose a reason for hiding this comment

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

This PR adds workspace support to packages check licenses, which is a useful feature with reasonable test coverage. Overall the approach (detect a workspace root, walk member pubspecs, build a Set of declared direct deps to filter pubspec.lock by name) is workable and backwards-compatible. A few concerns worth addressing:

Concerns

  • _collectWorkspaceDependencies recurses into nested workspaces with no cycle protection. A pubspec that lists its own directory (or a parent) in workspace: would cause unbounded recursion.
  • Pubspec.parse(..., lenient: true) accepts many malformed pubspecs without throwing. The malformed-pubspec test only passes because the YAML itself is unparsable; a structurally invalid but YAML-valid pubspec would silently parse with empty/default dep maps and contribute nothing — i.e. workspace members would be silently dropped.
  • PubspecWorkspace.isWorkspaceMember is dead production code (only referenced by tests). Either drop it or use it to validate that resolved members declare resolution: workspace.
  • The new workspace-aware filter and the helper that drives it (_collectWorkspaceDependencies) live in licenses.dart even though the logic is generic workspace-walking; moving it into lib/src/pubspec/pubspec.dart (or a sibling) would keep the command file focused and let other commands reuse it.
  • The branch around dependencyTypes.contains('direct-main') || dependencyTypes.contains('direct-dev') is redundant once workspaceDependencies was already filtered by those types — the outer if can be removed for clarity, or the membership Sets should be tracked per-type to actually enforce the distinction.
  • Test gaps: no coverage for (a) a dep declared as direct-main in one member and direct-dev in another (relevant since the merged Set loses that distinction), (b) _isGlobPattern's ?/[ branches, (c) cycle protection, and (d) a workspace member that also declares resolution: workspace mismatch.
  • _isGlobPattern is a hand-rolled heuristic; pkg:glob can be queried more directly, and patterns like !packages/foo (negation) won't be classified as glob.

Positives

  • Clean separation between pubspec.dart helpers and the command layer.
  • Backwards-compatibility is preserved: when no pubspec.yaml is present or it isn't a workspace root, the original filtering path is used unchanged.
  • Good integration test coverage for the common workspace shapes (direct-main/dev, transitive, overridden, nested, and non-workspace).


// Handle nested workspaces recursively
if (memberPubspec.isWorkspaceRoot) {
final nestedDeps = _collectWorkspaceDependencies(
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.

There is no cycle protection here. If a member's pubspec.yaml lists the parent (or itself) under workspace: — e.g. via a misconfigured glob like .. — this recursion will not terminate. Consider passing a Set<String> of canonicalized directory paths and short-circuiting if memberDirectory.absolute.path has already been visited.

// If we have workspace dependencies, use them for filtering direct deps
if (workspaceDependencies != null) {
// For direct-main and direct-dev, check against workspace dependencies
if (dependencyTypes.contains('direct-main') ||
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.

This outer if is redundant given the way workspaceDependencies is built — the Set only contains names whose declared type is in dependencyTypes, so the inner contains already encodes the filter. Either drop this branch (and just check workspaceDependencies.contains(dependency.name)) or, if you want to actually enforce direct-main vs direct-dev separately, change _collectWorkspaceDependencies to return per-type Sets. As written it implies a distinction that isn't really being made.

return false;
}

// Non-workspace: use the original filtering logic
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.

Subtle behavioral divergence worth calling out in a comment or doc: in non-workspace mode, direct-main is decided by pubspec.lock's recorded type; in workspace mode, it's decided by which members declare it. For overlapping packages (a dep that's direct dev in pubspec.lock but appears in some member's dependencies:) the two paths can disagree. Probably fine, but users will notice.

Comment thread lib/src/pubspec/pubspec.dart Outdated
bool get isWorkspaceRoot => workspace != null;

/// Whether this pubspec is a workspace member.
bool get isWorkspaceMember => resolution == _workspaceResolution;
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.

isWorkspaceMember isn't referenced anywhere in lib/; only the tests call it. Either delete it (YAGNI), or wire it into resolveWorkspaceMembers/_collectWorkspaceDependencies to warn/skip when a resolved member doesn't declare resolution: workspace — that would catch real misconfigurations rather than just being inert.

Comment thread lib/src/pubspec/pubspec.dart Outdated
return Pubspec.parse(
file.readAsStringSync(),
sourceUrl: file.uri,
lenient: true,
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.

lenient: true causes Pubspec.parse to accept many structurally invalid pubspecs without throwing. That means tryParsePubspec only returns null for true YAML-level errors — an otherwise well-formed YAML doc with bad dependencies: shape will parse to a Pubspec with empty maps and we'll silently skip its deps. The malformed-pubspec integration test exercises the YAML-error path; consider either dropping lenient: true (so structural errors are surfaced as well), or logging a warning when a workspace member fails to parse so users aren't left wondering why a member's deps are missing.

final members = <Directory>[];
for (final pattern in patterns) {
if (_isGlobPattern(pattern)) {
final matches = Glob(pattern).listSync(root: root.path);
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.

Workspace patterns under pub use POSIX-style separators, but on Windows users may write packages\app (or it may end up that way after normalization). Glob is POSIX-only — consider normalizing patterns to POSIX before constructing the Glob, or document the constraint.

}

return dependencies;
}
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.

This helper is purely about workspace traversal and doesn't depend on anything in this command — it would fit better in lib/src/pubspec/pubspec.dart (or a sibling) alongside resolveWorkspaceMembers, keeping the licenses command focused. That also makes it easier to unit-test directly rather than only through the integration tests.


expect(result, equals(ExitCode.success.code));
}),
);
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.

Missing test for the case where the same dep name appears as direct-main in one workspace member and direct-dev in another. Since _collectWorkspaceDependencies merges into a single Set<String>, this is exactly the case where the lost distinction could matter — worth a test that pins down the intended behavior.

test('returns null when the file contains invalid YAML', () {
final file = File(path.join(tempDirectory.path, pubspecBasename))
..writeAsStringSync('invalid: yaml: content: [');
expect(tryParsePubspec(file), isNull);
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.

This test passes because 'invalid: yaml: content: [' is unparseable at the YAML level. Given lenient: true, it does not actually exercise the case of a YAML-valid but pubspec-invalid file (e.g. name: foo\ndependencies: not-a-map). Worth adding a case for that to confirm whatever behavior you want — currently it would parse successfully and return an effectively-empty Pubspec.

}

// Check if this is a workspace root and collect dependencies accordingly
final pubspecFile = File(path.join(targetPath, pubspecBasename));
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.

Minor: you've already computed targetPath and targetDirectory. path.join(targetDirectory.path, pubspecBasename) would avoid recomputing the join from targetPath and keeps the single source of truth (targetDirectory). Same comment applies to pubspecLockFile above.

/// Attempts to read and parse a [Pubspec] from [file].
///
/// Returns `null` when the file does not exist or cannot be parsed.
Pubspec? tryParsePubspec(File file) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

additional to the bot's comments, I'd say all the pubspec related methods can be in the existing PubspecWorkspace extension since we're just extending pubspec_parse

- Move resolveWorkspaceMembers and dependency collection into the
  PubspecWorkspace extension on Pubspec, keeping pubspec helpers in the
  pubspec library.
- Add cycle protection to collectWorkspaceDependencies via a visited
  set of canonical paths to prevent unbounded recursion when a member
  references a parent.
- Drop the redundant outer dependency-type guard in licenses.dart now
  that workspaceDependencies is already filtered at collection time.
- Use targetDirectory.path consistently when joining the pubspec and
  pubspec.lock paths.
- Cover the merged-membership case (dep listed as direct-main in one
  member and direct-dev in another) and circular workspace references
  with new tests; rename groups to track the renamed APIs.
@marcossevilla
Copy link
Copy Markdown
Member

hey @realmeylisdev, will you be able to apply the comments on this PR? thanks!

- Drop `lenient: true` from `tryParsePubspec` so structurally invalid
  pubspecs (missing `name`, malformed dep maps) fall through to the
  exception handler instead of silently parsing with empty dep maps.
- Remove the unused `PubspecWorkspace.isWorkspaceMember` getter and
  `_workspaceResolution` constant; only tests referenced it.
- Drop the hand-rolled `_isGlobPattern` heuristic and route every
  workspace pattern through `Glob.listSync`. Literal paths match as
  exact entries, so the previous direct-path branch (which skipped
  the `pubspec.yaml` check) becomes unnecessary.
- Cover the new behavior: structurally invalid pubspecs, `?`/`[...]`/
  `{...}` glob shapes, literal paths with non-metachar symbols, and
  workspace members that omit `resolution: workspace`.
@realmeylisdev
Copy link
Copy Markdown
Contributor Author

@marcossevilla applied the remaining bot review nits in d5668ab:

  • Dropped lenient: true from tryParsePubspec so YAML-valid but structurally invalid pubspecs no longer silently parse with empty dep maps.
  • Removed the unused PubspecWorkspace.isWorkspaceMember getter (dead in lib/, only tests referenced it).
  • Replaced the hand-rolled _isGlobPattern heuristic with a single Glob.listSync path that handles literal and metachar patterns uniformly (so e.g. !packages/foo works without special-casing).
  • Filled the remaining test gaps: structurally invalid pubspec, ?/[…]/{…} glob shapes, literal paths with non-metachar symbols, and a member that omits resolution: workspace.

Ready for another look.

@marcossevilla
Copy link
Copy Markdown
Member

@vgvbot check the changes from your last review, resolve the comments that have been addressed and leave any comments if necessary

@very-good-code-bot
Copy link
Copy Markdown
Contributor

very-good-code-bot Bot commented May 20, 2026

I'll review the code and share my feedback shortly.

  • Preparing environment
    • Cloning repository
    • Setting up dev environment
  • Reviewing code
  • Posting review

Code review submitted.

Copy link
Copy Markdown
Contributor

@very-good-code-bot very-good-code-bot Bot left a comment

Choose a reason for hiding this comment

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

Overall

This PR adds workspace support to packages check licenses, which fills a real gap for monorepo users. The approach — walk member pubspecs to build a union of direct deps, then intersect with the workspace's pubspec.lock — is reasonable. Test coverage is broad, covering globs (*, ?, [...], {...}), nested workspaces, malformed members, and backwards compatibility.

Main concerns

  1. Duplicated filter logic in licenses.dart. The new workspace branch (lines 212–227) re-implements the transitive/direct-overridden checks that already exist in the non-workspace branch (lines 234–238). The two paths now have to stay in sync.
  2. Dual signal/data return in collectWorkspaceDependencies. null means "not a workspace," a non-null Set means "workspace deps." Since isWorkspaceRoot already exists as a separate getter, this overloading is unnecessary and makes the call site read awkwardly (workspaceDependencies != null is really asking "are we in workspace mode?").
  3. Redundant traversal on nested workspaces. When a member is itself a workspace root, its dependencies/devDependencies are added by the outer loop and by the recursive call's prologue. Set dedupes, but the data flow is harder to follow than it should be.
  4. dependency_overrides is silently ignored when collecting workspace deps. Members that override deps will rely on the lock file's direct-overridden marker — which is fine in practice but worth either documenting in the dartdoc or handling explicitly.
  5. Unnecessary churn at line 183: changing targetPath to targetDirectory.path doesn't change behavior (they're the same string) and bloats the diff.

Smaller notes

  • Several test fixtures collide on the pubspec name field (e.g. _workspaceMemberSharedPubspecContent is used for appDir, where it produces name: shared), which makes the test scenarios harder to parse.
  • tryParsePubspec catches Exception only; Pubspec.parse paths go through ParsedYamlException/YamlException which subclass FormatException, so this is correct — but the doc comment could state that explicitly.
  • Cross-platform: Glob(pattern).listSync(root: root.path) will use the platform path.Context by default. Workspace globs in real pubspecs always use /, which works, but worth a quick sanity check on Windows CI before shipping.


final pubspecLockFile = File(path.join(targetPath, pubspecLockBasename));
final pubspecLockFile = File(
path.join(targetDirectory.path, pubspecLockBasename),
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.

targetDirectory.path and targetPath are the same string here (targetDirectory = Directory(targetPath) two lines above). This rename adds diff noise without changing behavior — consider reverting so the diff stays focused on the workspace feature.

}

return false;
}
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.

The transitive and direct-overridden checks in this workspace branch duplicate the ones in the non-workspace branch below (lines 234–238). Now both paths need to be kept in sync whenever a new dependency-type is added. Consider folding this into a single filter that uses workspaceDependencies only to augment direct-main/direct-dev matching, e.g.:

final bool inWorkspaceDeps = workspaceDependencies?.contains(dependency.name) ?? false;
return (inWorkspaceDeps) ||
    (workspaceDependencies == null && dependencyTypes.contains('direct-main') && dependencyType == ...directMain) ||
    (workspaceDependencies == null && dependencyTypes.contains('direct-dev') && dependencyType == ...directDev) ||
    (dependencyTypes.contains('transitive') && dependencyType == ...transitive) ||
    (dependencyTypes.contains('direct-overridden') && dependencyType == ...directOverridden);

That keeps the transitive/overridden handling in one place.

final workspaceDependencies = pubspec?.collectWorkspaceDependencies(
root: targetDirectory,
dependencyTypes: dependencyTypes,
);
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.

Two coupled signals are being smuggled through workspaceDependencies: (a) whether the project is a workspace root and (b) the actual dep set. Consider making collectWorkspaceDependencies return a Set<String> unconditionally (empty when not a workspace) and read the boolean directly via pubspec?.isWorkspaceRoot ?? false. That removes the != null guard at the call site and the mixed-purpose null return in the extension.

}
if (dependencyTypes.contains('direct-dev')) {
deps.addAll(memberPubspec.devDependencies.keys);
}
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.

When memberPubspec is itself a workspace root, its dependencies/devDependencies get added here (lines 86–91), and then the recursive call at line 94 re-adds them via lines 72–77 of the recursion. The Set deduplicates so there's no correctness bug, but the data flow is confusing — readers have to convince themselves the duplicate add is harmless. Consider either skipping the direct add when memberPubspec.isWorkspaceRoot (let the recursion handle it) or having the recursive call only walk deeper members (i.e. drop lines 72–77 from the recursive path).

}
if (dependencyTypes.contains('direct-dev')) {
deps.addAll(memberPubspec.devDependencies.keys);
}
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.

dependencyOverrides is never inspected. In workspace mode, the filter relies on the lock file's direct-overridden marker to surface overrides, but a reader of this extension can't tell that from the code or doc. Worth either: (a) adding dependencyOverrides.keys here under a 'direct-overridden' branch for symmetry with the non-workspace path, or (b) calling out the intentional limitation in the dartdoc on collectWorkspaceDependencies.


final members = <Directory>[];
for (final pattern in patterns) {
final matches = Glob(pattern).listSync(root: root.path);
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.

Glob(pattern).listSync(root: root.path) uses the platform's default path.Context. Pub workspace globs are always POSIX-style (forward slashes) per the pub spec, but on Windows the default Context is windows. Worth either pinning Glob(pattern, context: p.posix) for portability, or at minimum verifying behavior on Windows CI before release. (Same applies to test fixtures using packages/app literals.)

} on Exception {
return null;
}
}
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.

Catching Exception is correct here (Pubspec.parse throws ParsedYamlException/YamlException, both FormatExceptions), but the dartdoc currently just says "cannot be parsed" — it's worth being explicit that this swallows all parse failures silently. Callers (notably collectWorkspaceDependencies) skip these members without logging, which could make broken member pubspecs invisible. Consider returning a richer signal or at least documenting that malformed members are silently skipped.

path.join(tempDirectory.path, 'packages', 'app'),
)..createSync(recursive: true);
File(path.join(appDir.path, 'pubspec.yaml')).writeAsStringSync(
_workspaceMemberSharedPubspecContent,
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.

This is confusing: _workspaceMemberSharedPubspecContent is named shared inside, but it's being written to appDir. So appDir/pubspec.yaml declares name: shared, and sharedDir/pubspec.yaml also declares name: shared (via _workspaceMemberCliCompletionDevDepPubspecContent, which also uses name: shared). Pub would reject duplicate names in a real workspace. Consider using a fixture whose name matches its directory, so the scenario reads naturally.


dev_dependencies:
very_good_analysis: ^5.0.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.

_workspaceMemberWithDevDepsPubspecContent declares both dependencies: (http) and dev_dependencies: (very_good_analysis). The name suggests dev-only deps. Consider renaming to _workspaceMemberWithMixedDepsPubspecContent (or similar) so it's obvious why this fixture is used in the dev-dependency-filter test.

dependencyTypes: ['direct-main'],
),
returnsNormally,
);
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.

returnsNormally only proves termination; it doesn't verify what was collected. Since this test exists to prove the cycle-prevention logic (seen.add(...)), assert on the returned set too — e.g. expect it to contain http from the cycling member but not enter an infinite loop. Otherwise this passes vacuously even if the early-return logic at line 68 of pubspec.dart regressed and started looping forever in a future change.

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.

feat: Support packages check licenses with workspaces

5 participants