diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc0892a..9666d1be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.31.3 - 2026-10-23 + +- Compile and lint governed Cargo replacement packages through their consuming + project, retain forced lint diagnostics, and omit warnings located entirely + inside external macro implementations while preserving caller diagnostics. +- Report the original pnpm failure when JavaScript license listing fails, + instead of treating its error response as a package inventory. + ## 0.31.2 - 2026-10-23 - Keep raw package scanner output in report artifacts and show bounded summaries. diff --git a/VERSION b/VERSION index c415e1c6..8239f42d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.31.2 +0.31.3 diff --git a/docs/design/cargo-facts.md b/docs/design/cargo-facts.md index 7e64dd03..990a1b47 100644 --- a/docs/design/cargo-facts.md +++ b/docs/design/cargo-facts.md @@ -281,8 +281,17 @@ handwritten lock or disabled source check cannot replace that boundary. ## Compiler checks -Compilation uses the matching pinned cargo-clippy executable directly, with an -absolute Cargo path. This bypasses Cargo's ambient external-subcommand lookup. +Compilation runs the pinned Cargo's `check` with the matching pinned +clippy-driver as `RUSTC_WRAPPER` and the policy lint arguments in +`CLIPPY_ARGS`, the driver interface cargo-clippy itself uses. Cargo-clippy is +not used: it installs the driver only as the workspace wrapper, so path +replacements outside the workspace would compile with plain rustc and escape +Clippy. The wrapper reaches every unit. Cargo still caps lints for registry and +Git packages, and the driver disables Clippy for those capped units, so +non-local code stays silent. Local path dependencies receive the same forced +lints even when they are outside the consumer's workspace; their diagnostics +can therefore refer to source beyond the selected files. The pinned driver is +selected explicitly; there is no ambient external-subcommand lookup. The common runner captures Cargo's JSON events with bounded output and process tree cancellation. Cargo must retain its reviewed lock and available dependency cache through `--frozen`; a missing lock is a preparation failure, never an @@ -290,14 +299,51 @@ invitation for the check to resolve dependencies. Checks select the manifest's ordinary Cargo packages, default features and all enabled targets, using the governed native host explicitly. They do not enable -every feature or establish coverage of other target configurations. Compiler -warnings, Clippy's default lint group and unused-code lints use `--force-warn` at -the invocation boundary. Source `allow`, `expect`, `deny` and `forbid` attributes -cannot hide these diagnostics or turn policy lint collection into a compiler -failure. Code Polishy treats the resulting typed findings as blocking errors, -subject to its existing exact exception policy. Failed compilation always adds -a project failure, even when the compiler also reports typed source diagnostics; -an excepted lint cannot make a failed build or operational invocation pass. +every feature or establish coverage of other target configurations. + +Compiler selection maps explicitly governed replacement packages to their consumers, +using the same ownership boundary as locked resolution. Discovery retains the +replacement's own metadata for source analysis and also discovers its consumer. +Selecting only replacement source still checks the consumer; selecting both +does not compile it twice. Replacement development targets and their independent +locks are not part of the consumer build. Source analysis remains selected for +the replacement, while compiler evidence covers only dependencies active in the +consumer's default feature and host configuration. Within that configuration, +replacement libraries receive the same Clippy, compiler +warning and unused-code collection as the consumer. + +Compiler warnings, Clippy's default lint group and unused-code lints use +`--force-warn` at the invocation boundary. Source `allow`, `expect`, `deny` and +`forbid` attributes, including attributes emitted by macro expansion, cannot +hide these diagnostics or turn policy lint collection into a compiler failure. +Forcing also bypasses rustc's suppression of lints in code generated by +external macros. A warning is therefore omitted when every one of its spans, +at its own location, is macro-expanded text outside project source. The governed +Cargo home contains downloaded registry and Git sources, not project source, +even though it lives under the repository. Both the caller's root spelling and +the physical root spelling retain this distinction. For +example, `std::pin::pin!` of an `async` block reports unreachable code whose +expression and cause both lie in the standard library's macro body. A warning +remains a finding when any span is repository text, including macro input +tokens, a local macro body or the user statement that makes later expanded code +unreachable. Errors are never omitted. + +Forcing also overrides the `allow(dead_code)` the test harness adds to a +binary's `main`. That attribute normally seeds dead-code reachability, so the +binary test build reports `main` and everything reachable only from it as +unused. These diagnostics are false and remain unresolved; no exception or +source-parser rule hides them. Cargo's compiler messages carry the same target +identity for the binary and its test build, and rustc reports these items +exactly as it reports a truly unused `cfg(test)` item. Distinguishing them +would require the emitting unit's test mode and either reachability provenance +or the item inventory of the ordinary build. A complete proof that +macro-expanded suppression is otherwise ineffective is also unavailable, so +native admission stays blocked on both. + +Code Polishy treats the resulting typed findings as blocking errors, subject to +its existing exact exception policy. Failed compilation always adds a project +failure, even when the compiler also reports typed source diagnostics; an +excepted lint cannot make a failed build or operational invocation pass. Rust does not support forcing the aggregate `warnings` group. The release owns the concrete warning-by-default lint inventory for its pinned compiler, checked against that compiler's advertised lint interface during toolchain tests. Rust diff --git a/internal/quality/cargo_test.go b/internal/quality/cargo_test.go index 635c6a44..3342c151 100644 --- a/internal/quality/cargo_test.go +++ b/internal/quality/cargo_test.go @@ -82,6 +82,32 @@ func TestCargoAndGoCheckPlanMatchesObservedCommandOrder(t *testing.T) { } } +func TestCargoReplacementCheckKeepsGoCommandsInsideThePlan(t *testing.T) { + t.Parallel() + repo := cargoQualityRepository(t) + repo.Config.SupplyChain.DependencyOverridePolicies = []policy.DependencyOverridePolicy{{Ecosystem: "cargo", Path: "Cargo.toml", Field: "patch.crates-io"}} + writeQualityFile(t, repo.Root, "Cargo.toml", "[package]\nname='app'\nversion='1.0.0'\nedition='2024'\n[workspace]\nexclude=['vendor/library']\n[dependencies]\nlibrary='=1.0.0'\n[patch.crates-io]\nlibrary={path='vendor/library'}\n") + writeQualityFile(t, repo.Root, "Cargo.lock", "version=4\n[[package]]\nname='app'\nversion='1.0.0'\ndependencies=['library']\n[[package]]\nname='library'\nversion='1.0.0'\n") + writeQualityFile(t, repo.Root, "src/lib.rs", "pub fn app() {\n library::call();\n}\n") + writeQualityFile(t, repo.Root, "vendor/library/Cargo.toml", "[package]\nname='library'\nversion='1.0.0'\nedition='2024'\n") + writeQualityFile(t, repo.Root, "vendor/library/src/lib.rs", "pub fn call() {}\n") + writeQualityFile(t, repo.Root, "go.mod", "module example.test/mixed\n\ngo 1.26.6\n") + writeQualityFile(t, repo.Root, "main.go", "package main\n\nfunc main() {}\n") + selection := repository.Selection{Files: []string{"main.go", "src/lib.rs", "vendor/library/src/lib.rs"}} + planned := CheckCommands(repo, selection, "check") + boundary := &cargoRecordingRunner{OSRunner: runner.OSRunner{}} + if findings := Check(t.Context(), repo, selection, boundary, "check"); len(findings) != 0 { + t.Fatalf("replacement prevented the mixed-language check: %+v", findings) + } + want := []string{} + for _, command := range planned { + want = append(want, command.Name) + } + if !reflect.DeepEqual(boundary.names, want) { + t.Fatalf("executed commands = %v, planned = %v", boundary.names, want) + } +} + func TestCargoNativeDeadCodePreservesPublicAPIAndChecksSelectedFeatures(t *testing.T) { t.Parallel() repo := cargoQualityRepository(t) diff --git a/internal/rustfacts/compilation.go b/internal/rustfacts/compilation.go index e3e4fa41..3bda8a4a 100644 --- a/internal/rustfacts/compilation.go +++ b/internal/rustfacts/compilation.go @@ -26,7 +26,9 @@ func PlanCompilation(repo repository.Repository, discovery DiscoveryPlan, allFil if len(discovery.Commands) == 0 { return plan } - for _, manifest := range discovery.Manifests { + manifests, _, findings := cargoResolutionRoots(repo, discovery.Manifests) + plan.Findings = append(plan.Findings, findings...) + for _, manifest := range manifests { files := compilationSources(repo, manifest, allFiles) native, err := repo.NativeUnit(files, "lint", "typecheck", "dead-code") if err != nil { @@ -58,15 +60,22 @@ func compilationSources(repo repository.Repository, manifest string, allFiles [] } func compilationCommand(repo repository.Repository, manifest string) policy.Command { - command := Command(repo, manifestCommandName("policy-clippy", manifest), "cargo-clippy", "clippy", "--frozen", "--all-targets", "--no-deps", "--message-format=json", "--target", HostTarget(), "--manifest-path", manifest, "--", "--force-warn", "clippy::all", "--force-warn", "unused") - for _, lint := range strings.Fields(compilerWarningLints) { - command.Argv = append(command.Argv, "--force-warn", lint) - } + command := Command(repo, manifestCommandName("policy-clippy", manifest), "cargo", "check", "--frozen", "--all-targets", "--message-format=json", "--target", HostTarget(), "--manifest-path", manifest) command.Environment = append(command.Environment, "PATH", "LIB", "INCLUDE", "LIBPATH", "SDKROOT", "DEVELOPER_DIR") - command.EnvironmentOverrides = append(command.EnvironmentOverrides, "CARGO="+repo.RustTool("cargo"), "CLIPPY_CONF_DIR="+filepath.Join(repo.PolicyRoot, "tools")) + command.EnvironmentOverrides = append(command.EnvironmentOverrides, "RUSTC_WRAPPER="+repo.RustTool("clippy-driver"), "CLIPPY_ARGS="+strings.Join(compilationLintArguments(), clippyArgumentSeparator)+clippyArgumentSeparator, "CLIPPY_CONF_DIR="+filepath.Join(repo.PolicyRoot, "tools")) return command } +func compilationLintArguments() []string { + arguments := []string{"--force-warn", "clippy::all", "--force-warn", "unused"} + for _, lint := range strings.Fields(compilerWarningLints) { + arguments = append(arguments, "--force-warn", lint) + } + return arguments +} + +const clippyArgumentSeparator = "__CLIPPY_HACKERY__" + func HostTarget() string { architecture := map[string]string{"amd64": "x86_64", "arm64": "aarch64"}[runtime.GOARCH] platform := map[string]string{"linux": "unknown-linux-gnu", "darwin": "apple-darwin", "windows": "pc-windows-msvc"}[runtime.GOOS] diff --git a/internal/rustfacts/compilation_patches_test.go b/internal/rustfacts/compilation_patches_test.go new file mode 100644 index 00000000..7b183496 --- /dev/null +++ b/internal/rustfacts/compilation_patches_test.go @@ -0,0 +1,100 @@ +package rustfacts + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "slices" + "testing" + + "github.com/riteofstring/code-polishy/internal/policy" + "github.com/riteofstring/code-polishy/internal/repository" + "github.com/riteofstring/code-polishy/internal/runner" +) + +func TestRustCompilationUsesReplacementConsumerAndPreservesDependencyTargets(t *testing.T) { + t.Parallel() + for _, selected := range []string{"Cargo.toml", "vendor/patch-source/src/lib.rs", "vendor/leaf/src/lib.rs"} { + t.Run(selected, func(t *testing.T) { + repo := consumedReplacementRepository(t) + write(t, repo.Root, "vendor/patch-source/src/lib.rs", "pub fn value() -> u32 { patch_leaf::value() }\n") + write(t, repo.Root, "vendor/leaf/src/lib.rs", "pub fn value() -> u32 { 42 }\n") + before := readRustFile(t, repo, "Cargo.lock") + plan, discovery := compilationDiscovery(t, repo, selected) + if findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}); len(findings) != 0 { + t.Fatalf("consumer compilation failed: %+v", findings) + } + if !bytes.Equal(before, readRustFile(t, repo, "Cargo.lock")) { + t.Fatal("compilation changed the consumer lock") + } + for _, path := range []string{"vendor/patch-source/Cargo.lock", "vendor/leaf/Cargo.lock"} { + if _, err := os.Stat(filepath.Join(repo.Root, filepath.FromSlash(path))); !os.IsNotExist(err) { + t.Fatalf("compilation created a replacement lock at %s: %v", path, err) + } + } + write(t, repo.Root, "vendor/leaf/src/lib.rs", "pub fn value() -> u32 { \"wrong\" }\n") + findings := Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) + if !slices.ContainsFunc(findings, func(f policy.Finding) bool { + return f.Check == "quality.rustCompiler" && f.Subject == "E0308" && f.Path == "vendor/leaf/src/lib.rs" + }) { + t.Fatalf("consumer failed to check the replacement source: %+v", findings) + } + }) + } +} + +func TestRustCompilationLintsGovernedReplacementsAndCapsNonLocalDependencies(t *testing.T) { + t.Parallel() + repo := consumedReplacementRepository(t) + remote := t.TempDir() + write(t, remote, "Cargo.toml", "[package]\nname='remote'\nversion='1.0.0'\nedition='2021'\n") + write(t, remote, "src/lib.rs", `pub fn value() -> u32 { if needless(true) { 1 } else { 2 } } +fn needless(value: bool) -> bool { if value { true } else { false } } +fn remote_unused() {} +#[macro_export] +macro_rules! remote_stop { () => {{ return 0u32; 1u32 }}; } +`) + for _, arguments := range [][]string{{"init", "-q"}, {"add", "."}, {"-c", "user.name=fixture", "-c", "user.email=fixture@example.test", "commit", "-qm", "fixture"}} { + if output, err := exec.Command("git", append([]string{"-C", remote}, arguments...)...).CombinedOutput(); err != nil { + t.Fatalf("git fixture: %v; %s", err, output) + } + } + manifest := string(readRustFile(t, repo, "Cargo.toml")) + write(t, repo.Root, "Cargo.toml", string(bytes.Replace([]byte(manifest), []byte("[patch.crates-io]"), []byte("remote={git='file://"+filepath.ToSlash(remote)+"'}\n[patch.crates-io]"), 1))) + write(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { patch_source::value() + remote::value() }\npub fn remote_macro() -> u32 { remote::remote_stop!() }\n") + write(t, repo.Root, "vendor/patch-source/src/lib.rs", "pub fn value() -> u32 { if needless(true) { patch_leaf::value() } else { 2 } }\nfn needless(value: bool) -> bool { if value { true } else { false } }\n#[allow(dead_code)]\nfn source_unused() {}\n") + write(t, repo.Root, "vendor/leaf/src/lib.rs", "macro_rules! quiet { ($i:item) => { #[allow(dead_code, clippy::needless_bool)] $i }; }\nquiet! { fn leaf_needless(value: bool) -> bool { if value { true } else { false } } }\npub fn value() -> u32 { 42 }\n") + lock := exec.Command(repo.RustTool("cargo"), "generate-lockfile", "--manifest-path", filepath.Join(repo.Root, "Cargo.toml")) + lock.Env = append(os.Environ(), "CARGO_HOME="+CargoHome(repo), "RUSTC="+repo.RustTool("rustc")) + if output, err := lock.CombinedOutput(); err != nil { + t.Fatalf("fixture lock: %v; %s", err, output) + } + plan, discovery := compilationDiscovery(t, repo, "vendor/patch-source/src/lib.rs") + reported := [][3]string{} + for _, finding := range Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) { + reported = append(reported, [3]string{finding.Check, finding.Subject, finding.Path}) + } + slices.SortFunc(reported, func(left, right [3]string) int { return slices.Compare(left[:], right[:]) }) + reported = slices.Compact(reported) + want := [][3]string{ + {"quality.clippy", "clippy::needless_bool", "vendor/leaf/src/lib.rs"}, + {"quality.clippy", "clippy::needless_bool", "vendor/patch-source/src/lib.rs"}, + {"quality.rustCompiler", "dead_code", "vendor/leaf/src/lib.rs"}, + {"quality.rustCompiler", "dead_code", "vendor/patch-source/src/lib.rs"}, + } + if !slices.Equal(reported, want) { + t.Fatalf("replacement lint findings = %v, want %v", reported, want) + } +} + +func consumedReplacementRepository(t *testing.T) repository.Repository { + t.Helper() + repo := cargoPatchRepository(t) + manifest := string(readRustFile(t, repo, "Cargo.toml")) + write(t, repo.Root, "Cargo.toml", string(bytes.ReplaceAll([]byte(manifest), []byte("default=[]"), []byte("default=['desktop']")))) + write(t, repo.Root, "src/lib.rs", "pub fn value() -> u32 { patch_source::value() }\n") + write(t, repo.Root, "vendor/patch-source/build.rs", "fn main() {}\n") + write(t, repo.Root, "vendor/patch-source/tests/unconsumed.rs", "compile_error!(\"dependency development targets must not compile\");\n") + return repo +} diff --git a/internal/rustfacts/compilation_test.go b/internal/rustfacts/compilation_test.go index 89aaca25..e86da7ba 100644 --- a/internal/rustfacts/compilation_test.go +++ b/internal/rustfacts/compilation_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "os" "path/filepath" "slices" @@ -61,6 +62,9 @@ func TestRustCompilationCollectsLintsDespiteSourceLevels(t *testing.T) { "allow unused": {"#[allow(dead_code)]\nfn unused() {}\npub fn answer() {}\n", "dead_code"}, "expect unused": {"#[expect(dead_code)]\nfn unused() {}\npub fn answer() {}\n", "dead_code"}, "allow warning": {"#[allow(unreachable_code)]\npub fn answer() { return; println!(\"unreachable\"); }\n", "unreachable_code"}, + "macro clippy": {"macro_rules! quiet { ($i:item) => { #[allow(dead_code, clippy::needless_bool)] $i }; }\nquiet! { fn unused(value: bool) -> bool { if value { true } else { false } } }\npub fn answer() {}\n", "clippy::needless_bool"}, + "macro unused": {"macro_rules! quiet { ($i:item) => { #[allow(dead_code, clippy::needless_bool)] $i }; }\nquiet! { fn unused(value: bool) -> bool { if value { true } else { false } } }\npub fn answer() {}\n", "dead_code"}, + "meta unused": {"macro_rules! with { ($m:meta, $i:item) => { #[$m] $i }; }\nwith!(allow(dead_code), fn unused() {});\npub fn answer() {}\n", "dead_code"}, } { t.Run(name, func(t *testing.T) { repo := compilationRepository(t, test.source) @@ -82,6 +86,30 @@ func TestRustCompilationCollectsLintsDespiteSourceLevels(t *testing.T) { } } +func TestRustCompilationOmitsLibraryExpansionWarningsAndKeepsRepositoryCode(t *testing.T) { + t.Parallel() + repo := compilationRepository(t, `macro_rules! stop { () => {{ return 0; 1 }} } +pub fn local_macro() -> u32 { stop!() } +pub fn after_todo() -> u32 { todo!(); 1 } +fn really_unused() {} +#[cfg(test)] +fn test_helper_unused() {} +pub fn pinned() -> usize { let future = std::pin::pin!(async { 1u32 }); std::mem::size_of_val(&future) } +pub fn pinned_input() -> usize { let future = std::pin::pin!(async { return 1u32; 2u32 }); std::mem::size_of_val(&future) } +`) + plan, discovery := compilationDiscovery(t, repo, "src/lib.rs") + reported := []string{} + for _, finding := range Compile(t.Context(), repo, plan, discovery, runner.OSRunner{}) { + reported = append(reported, fmt.Sprintf("%s %s:%d", finding.Subject, finding.Path, finding.Line)) + } + slices.Sort(reported) + reported = slices.Compact(reported) + want := []string{"dead_code src/lib.rs:4", "dead_code src/lib.rs:6", "unreachable_code src/lib.rs:1", "unreachable_code src/lib.rs:3", "unreachable_code src/lib.rs:8"} + if !slices.Equal(reported, want) { + t.Fatalf("findings = %v, want %v", reported, want) + } +} + func TestRustCompilationFailureCannotBecomeAnExceptableLint(t *testing.T) { t.Parallel() for name, test := range map[string]struct{ source, build, subject string }{ diff --git a/internal/rustfacts/compiler_diagnostics.go b/internal/rustfacts/compiler_diagnostics.go index b245fa72..c470b6ff 100644 --- a/internal/rustfacts/compiler_diagnostics.go +++ b/internal/rustfacts/compiler_diagnostics.go @@ -24,6 +24,9 @@ func compilerFindings(repo repository.Repository, manifest string, workspace Wor default: return nil, errors.New("cargo compiler diagnostic has an unsupported severity") } + if diagnostic.Level == "warning" && externalExpansion(repo, workspace, diagnostic.Spans) { + return nil, nil + } code, check := "rustc", "quality.rustCompiler" if diagnostic.Code != nil && diagnostic.Code.Code != "" { code = diagnostic.Code.Code @@ -74,3 +77,29 @@ func compilerLocation(repo repository.Repository, workspace Workspace, span comp } return policy.FindingLocation{}, false } + +func externalExpansion(repo repository.Repository, workspace Workspace, spans []compilerSpan) bool { + primary := false + for _, span := range spans { + path := span.File + if !filepath.IsAbs(path) { + path = filepath.Join(repo.Root, filepath.FromSlash(workspace.Root), path) + } + if span.Expansion == nil || repositorySource(repo, path) { + return false + } + primary = primary || span.Primary + } + return primary +} + +func repositorySource(repo repository.Repository, path string) bool { + physical, _ := filepath.EvalSymlinks(repo.Root) + for _, root := range []string{repo.Root, physical} { + if root != "" && withinDirectory(root, path) { + repo.Root = root + return !withinDirectory(CargoHome(repo), path) + } + } + return false +} diff --git a/internal/rustfacts/compiler_output_test.go b/internal/rustfacts/compiler_output_test.go index 894685eb..0498c1cb 100644 --- a/internal/rustfacts/compiler_output_test.go +++ b/internal/rustfacts/compiler_output_test.go @@ -60,6 +60,45 @@ func TestCargoCompilerDiagnosticsRetainMacroAndUnavailableLocations(t *testing.T } } +func TestCargoCompilerWarningsOmitOnlyExpansionsOutsideTheRepository(t *testing.T) { + t.Parallel() + repo, workspace, _ := compilerOutputFixture(t) + target := workspace.Packages[0].Targets[0] + target.Path = filepath.Join(repo.Root, target.Path) + library := compilerSpan{File: "/rustc/toolchain/library/core/src/pin.rs", Line: 9, Column: 9, Primary: true, Expansion: &compilerExpansion{Span: compilerSpan{File: "src/lib.rs", Line: 1, Column: 8}}} + repository := compilerSpan{File: "src/lib.rs", Line: 2, Column: 3, Primary: true} + cause := compilerSpan{File: "src/lib.rs", Line: 1, Column: 18} + libraryCause := compilerSpan{File: library.File, Line: 2, Column: 9, Expansion: library.Expansion} + unexpanded := compilerSpan{File: "/unavailable/dependency.rs", Line: 5, Column: 3, Primary: true} + cached := library + cached.File = filepath.Join(CargoHome(repo), "registry", "src", "source-id", "dependency-1.0.0", "src", "lib.rs") + nearCache := library + nearCache.File = filepath.Join(CargoHome(repo)+"-local", "src", "lib.rs") + for name, test := range map[string]struct { + level string + spans []compilerSpan + kept bool + }{ + "library expansion": {"warning", []compilerSpan{library, libraryCause}, false}, + "repository cause": {"warning", []compilerSpan{library, cause}, true}, + "library and repository": {"warning", []compilerSpan{library, repository}, true}, + "outside without expansion": {"warning", []compilerSpan{unexpanded}, true}, + "library expansion error": {"error", []compilerSpan{library}, true}, + "cached registry expansion": {"warning", []compilerSpan{cached}, false}, + "cached caller input": {"warning", []compilerSpan{cached, repository}, true}, + "cached expansion error": {"error", []compilerSpan{cached}, true}, + "similar local directory": {"warning", []compilerSpan{nearCache}, true}, + } { + t.Run(name, func(t *testing.T) { + event := compilerEvent{Reason: "compiler-message", PackageID: workspace.Packages[0].ID, Target: target, Message: compilerDiagnostic{Message: "seeded diagnostic", Level: test.level, Code: &compilerCode{Code: "unreachable_code"}, Spans: test.spans}} + findings, err := compilerFindings(repo, "Cargo.toml", workspace, event) + if err != nil || (len(findings) == 1) != test.kept { + t.Fatalf("findings = %+v, %v; kept = %v", findings, err, test.kept) + } + }) + } +} + func TestCargoCompilerOutputRetainsDiagnosticsBeforeTruncation(t *testing.T) { t.Parallel() repo, workspace, _ := compilerOutputFixture(t) diff --git a/internal/rustfacts/discovery.go b/internal/rustfacts/discovery.go index b7e66a40..a2006620 100644 --- a/internal/rustfacts/discovery.go +++ b/internal/rustfacts/discovery.go @@ -29,6 +29,11 @@ func PlanDiscovery(repo repository.Repository, selected, allFiles []string) Disc if len(manifests) == 0 { return plan } + consumers, _, findings := cargoResolutionRoots(repo, manifests) + plan.Findings = append(plan.Findings, findings...) + manifests = append(manifests, consumers...) + slices.Sort(manifests) + manifests = slices.Compact(manifests) if err := configurationProblem(repo); err != nil { plan.Findings = append(plan.Findings, projectFinding(manifests[0], "configuration", err.Error())) return plan diff --git a/internal/rustfacts/lints_test.go b/internal/rustfacts/lints_test.go index 72810d12..78eb6634 100644 --- a/internal/rustfacts/lints_test.go +++ b/internal/rustfacts/lints_test.go @@ -16,7 +16,7 @@ func TestRustLintCollectionCoversCompilerWarningsWithoutLoweringErrors(t *testin t.Fatalf("compiler lint interface: %v; %s", err, output.Stderr) } forced := map[string]bool{} - arguments := compilationCommand(repo, "Cargo.toml").Argv + arguments := compilationLintArguments() for index, argument := range arguments[:len(arguments)-1] { if argument == "--force-warn" { forced[arguments[index+1]] = true diff --git a/scripts/check-javascript-bundle.sh b/scripts/check-javascript-bundle.sh index 0d48eb4b..bbb6b6b7 100755 --- a/scripts/check-javascript-bundle.sh +++ b/scripts/check-javascript-bundle.sh @@ -46,11 +46,21 @@ license_flattener="${temporary_dir}/flatten-licenses.mjs" cat >"${license_flattener}" <<'EOF' let raw = ""; process.stdin.on("data", (chunk) => (raw += chunk)).on("end", () => { + const report = JSON.parse(raw); + if (report.error) { + process.stderr.write(`pnpm could not list the bundle licenses: ${report.error.code}: ${report.error.message}\n`); + process.exit(1); + } const rows = []; - for (const [license, packages] of Object.entries(JSON.parse(raw))) + for (const [license, packages] of Object.entries(report)) { + if (!Array.isArray(packages)) { + process.stderr.write(`pnpm reported an unreadable license list for ${license}\n`); + process.exit(1); + } for (const entry of packages) for (const version of entry.versions) rows.push(`${entry.name}@${version}\t${license}`); + } process.stdout.write(rows.sort().join("\n") + "\n"); }); EOF