Skip to content

Commit 50ddd72

Browse files
grievejiameta-codesync[bot]
authored andcommitted
Connect Bazel sourcedb to the bazel-check command
Summary: _integration_ The `bazel-check` subcommand was only a scaffold: it parsed the Bazel input and assembled the source database, but then always wrote an empty diagnostic file and reported success. This change makes it actually type check the target. Results are surfaced in the two forms the build integration needs: a compact, per-target JSON file — each diagnostic tagged with the target label and an openable on-disk path — for downstream tooling, and human-readable rendered diagnostics on stderr under a target header for anyone reading the log. The check runs hermetically: interpreter querying and search-path heuristics are disabled, so the outcome depends only on the source database and search paths Bazel provides, keeping results reproducible under the build. `--min-severity` gates what is emitted, but directives such as `reveal_type` always survive the filter; the command exits with a user error only when a genuine diagnostic — not a directive — meets the threshold, so informational output never fails a build. Reviewed By: stroxler Differential Revision: D110555019 fbshipit-source-id: 230b5d697071683ef76c2018ed389c83d897c035
1 parent fb3e177 commit 50ddd72

3 files changed

Lines changed: 213 additions & 16 deletions

File tree

pyrefly/lib/commands/bazel_check.rs

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use pyrefly_python::module_path::ModuleStyle;
2929
use pyrefly_python::sys_info::PythonPlatform;
3030
use pyrefly_python::sys_info::PythonVersion;
3131
use pyrefly_python::sys_info::SysInfo;
32+
use pyrefly_util::arc_id::ArcId;
3233
use pyrefly_util::fs_anyhow;
3334
use pyrefly_util::thread_pool::ThreadCount;
3435
use serde::Deserialize;
@@ -37,7 +38,14 @@ use serde_json::Value;
3738
use starlark_map::small_map::SmallMap;
3839
use vec1::Vec1;
3940

41+
use crate::commands::check::write_errors_to_stderr;
4042
use crate::commands::util::CommandExitStatus;
43+
use crate::config::config::ConfigFile;
44+
use crate::config::finder::ConfigFinder;
45+
use crate::error::error::Error;
46+
use crate::error::legacy::LegacyError;
47+
use crate::state::require::Require;
48+
use crate::state::state::State;
4149

4250
/// Arguments for Bazel-powered type checking.
4351
#[derive(Debug, Clone, Parser)]
@@ -793,8 +801,65 @@ impl ModuleEnumerator for BazelCheckSourceDatabase {
793801
}
794802

795803
#[derive(Debug, Serialize)]
796-
struct BazelDiagnostics {
797-
diagnostics: Vec<Value>,
804+
struct BazelDiagnostics<'a> {
805+
diagnostics: Vec<BazelDiagnostic<'a>>,
806+
}
807+
808+
#[derive(Debug, Serialize)]
809+
struct BazelDiagnostic<'a> {
810+
target: &'a str,
811+
#[serde(flatten)]
812+
error: LegacyError,
813+
}
814+
815+
fn keep_in_bazel_output(error: &Error, min_severity: Severity) -> bool {
816+
error.error_kind().is_directive() || error.severity() >= min_severity
817+
}
818+
819+
fn compute_bazel_errors(
820+
parsed_config: &ParsedBazelConfig,
821+
search_roots: &[BazelSearchRoot],
822+
source_db: BazelCheckSourceDatabase,
823+
thread_count: ThreadCount,
824+
) -> anyhow::Result<Vec<Error>> {
825+
let modules_to_check = source_db.modules_to_check();
826+
let mut config = ConfigFile::default();
827+
config.python_environment.python_platform = Some(parsed_config.system_platform.clone());
828+
config.python_environment.python_version = Some(parsed_config.python_version);
829+
config.python_environment.site_package_path = Some(Vec::new());
830+
config.search_path_from_file = search_roots
831+
.iter()
832+
.map(|root| root.physical.clone())
833+
.collect();
834+
config.source_db = Some(ArcId::new(Box::new(source_db)));
835+
config.interpreters.skip_interpreter_query = true;
836+
config.disable_search_path_heuristics = true;
837+
config.preset = parsed_config.preset;
838+
if let Some(errors) = &parsed_config.errors {
839+
config.root.errors = Some(errors.clone());
840+
}
841+
config.configure();
842+
843+
let config = ArcId::new(config);
844+
let state = State::new(ConfigFinder::new_constant(config), thread_count);
845+
let mut transaction = state.new_transaction(Require::Errors, None);
846+
transaction.run(&modules_to_check, Require::Errors, None);
847+
848+
Ok(transaction
849+
.get_errors(&modules_to_check)
850+
.collect_display_errors_with_unused_ignores())
851+
}
852+
853+
fn bazel_diagnostics_from_errors<'a>(target: &'a str, errors: &[Error]) -> BazelDiagnostics<'a> {
854+
BazelDiagnostics {
855+
diagnostics: errors
856+
.iter()
857+
.map(|error| BazelDiagnostic {
858+
target,
859+
error: LegacyError::from_error(Path::new(""), error),
860+
})
861+
.collect(),
862+
}
798863
}
799864

800865
fn read_input_file(path: &Path) -> anyhow::Result<BazelCheckInput> {
@@ -803,15 +868,15 @@ fn read_input_file(path: &Path) -> anyhow::Result<BazelCheckInput> {
803868
BazelCheckInput::from_json_bytes(path, &data)
804869
}
805870

806-
fn write_output(path: &Path, diagnostics: &BazelDiagnostics) -> anyhow::Result<()> {
871+
fn write_output(path: &Path, diagnostics: &BazelDiagnostics<'_>) -> anyhow::Result<()> {
807872
let output_bytes = serde_json::to_vec(diagnostics)
808873
.with_context(|| "failed to serialize Bazel diagnostic JSON value to bytes")?;
809874
fs_anyhow::write(path, &output_bytes)
810875
}
811876

812877
impl BazelCheckArgs {
813-
pub fn run(self, _thread_count: ThreadCount) -> anyhow::Result<CommandExitStatus> {
814-
match self.run_inner() {
878+
pub fn run(self, thread_count: ThreadCount) -> anyhow::Result<CommandExitStatus> {
879+
match self.run_inner(thread_count) {
815880
Ok(status) => Ok(status),
816881
Err(error) => {
817882
eprintln!("{error:?}");
@@ -820,29 +885,41 @@ impl BazelCheckArgs {
820885
}
821886
}
822887

823-
fn run_inner(self) -> anyhow::Result<CommandExitStatus> {
888+
fn run_inner(self, thread_count: ThreadCount) -> anyhow::Result<CommandExitStatus> {
824889
let Self {
825890
input_path,
826891
output_path,
827-
min_severity: _,
892+
min_severity,
828893
} = self;
829894
let input = read_input_file(&input_path)?;
830895
let file_inputs = build_bazel_file_inputs(&input)?;
831896
let config = input.config.parse()?;
832897
let search_roots = build_search_roots(&input.search_path, config.python_version)?;
833898
let explicit_entries =
834899
build_explicit_entries(file_inputs, &search_roots, &input.target.label)?;
835-
let _source_db = BazelCheckSourceDatabase::new(
900+
let source_db = BazelCheckSourceDatabase::new(
836901
explicit_entries,
837-
SysInfo::new(config.python_version, config.system_platform),
902+
SysInfo::new(config.python_version, config.system_platform.clone()),
838903
);
839-
write_output(
840-
&output_path,
841-
&BazelDiagnostics {
842-
diagnostics: Vec::new(),
843-
},
844-
)?;
845-
Ok(CommandExitStatus::Success)
904+
let type_errors = compute_bazel_errors(&config, &search_roots, source_db, thread_count)?;
905+
let displayed_errors = type_errors
906+
.into_iter()
907+
.filter(|error| keep_in_bazel_output(error, min_severity))
908+
.collect::<Vec<_>>();
909+
let diagnostics = bazel_diagnostics_from_errors(&input.target.label, &displayed_errors);
910+
write_output(&output_path, &diagnostics)?;
911+
if !displayed_errors.is_empty() {
912+
eprintln!("Target {}", input.target.label);
913+
write_errors_to_stderr(Path::new(""), &displayed_errors, true)?;
914+
}
915+
if displayed_errors
916+
.iter()
917+
.any(|error| !error.error_kind().is_directive())
918+
{
919+
Ok(CommandExitStatus::UserError)
920+
} else {
921+
Ok(CommandExitStatus::Success)
922+
}
846923
}
847924
}
848925

pyrefly/lib/commands/check.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::time::Duration;
1919
use std::time::Instant;
2020

2121
use anstream::eprintln;
22+
use anstream::stderr;
2223
use anstream::stdout;
2324
use anyhow::Context as _;
2425
use clap::Parser;
@@ -482,6 +483,22 @@ fn write_error_text_to_console(
482483
Ok(())
483484
}
484485

486+
pub(crate) fn write_errors_to_stderr(
487+
relative_to: &Path,
488+
errors: &[Error],
489+
verbose: bool,
490+
) -> anyhow::Result<()> {
491+
let stderr = stderr();
492+
let color_choice = stderr.current_choice();
493+
let mut renderer = ErrorRenderer::new(BufWriter::new(stderr.lock()), color_choice);
494+
for error in errors {
495+
renderer.write(error, relative_to, verbose)?;
496+
renderer.flush()?;
497+
}
498+
renderer.flush()?;
499+
Ok(())
500+
}
501+
485502
fn write_error_json(
486503
writer: &mut impl Write,
487504
relative_to: &Path,

test/bazel_check.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,106 @@ $ $PYREFLY --help | grep -F "bazel-check"
77
*bazel-check* (glob)
88
[0]
99
```
10+
11+
## Direct root error includes target in stderr and JSON
12+
13+
```scrut {output_stream: stdout}
14+
$ mkdir -p $TMPDIR/bazel_error/pkg && cd $TMPDIR/bazel_error && \
15+
> echo "x: int = 'bad'" > pkg/app.py && \
16+
> printf '%s' '{"target":{"label":"//pkg:app","workspace_name":"_main","package":"pkg","name":"app","rule_kind":"py_binary"},"check_roots":{"sources":["pkg/app.py"]},"search_path":{"workspace_name":"_main","python_import_all_repositories":false},"config":{"python_version":"3.12","system_platform":"linux"}}' > input.json && \
17+
> $PYREFLY bazel-check --output out.json input.json > stdout.txt 2> stderr.txt; rc=$?; \
18+
> test ! -s stdout.txt && \
19+
> cat stderr.txt && \
20+
> $JQ -r '[(.diagnostics | length), .diagnostics[0].target, .diagnostics[0].path, .diagnostics[0].name] | join(" ")' out.json; \
21+
> exit $rc
22+
Target //pkg:app
23+
ERROR * [bad-assignment] (glob)
24+
*pkg/app.py* (glob)
25+
* (glob+)
26+
1 //pkg:app pkg/app.py bad-assignment
27+
[1]
28+
```
29+
30+
## Dependency files are importable but not checked as roots
31+
32+
```scrut {output_stream: stdout}
33+
$ mkdir -p $TMPDIR/bazel_dep/pkg && cd $TMPDIR/bazel_dep && \
34+
> echo "import pkg.dep" > pkg/app.py && \
35+
> echo "x: int = 'bad'" > pkg/dep.py && \
36+
> printf '%s' '{"target":{"label":"//pkg:app","workspace_name":"_main","package":"pkg","name":"app","rule_kind":"py_binary"},"check_roots":{"sources":["pkg/app.py"]},"search_path":{"workspace_name":"_main","python_import_all_repositories":false},"config":{"python_version":"3.12","system_platform":"linux"}}' > input.json && \
37+
> $PYREFLY bazel-check --output out.json input.json > stdout.txt 2> stderr.txt && \
38+
> test ! -s stdout.txt && test ! -s stderr.txt && \
39+
> $JQ '.diagnostics | length' out.json
40+
0
41+
[0]
42+
```
43+
44+
## Dependency type can produce a root diagnostic
45+
46+
```scrut {output_stream: stdout}
47+
$ mkdir -p $TMPDIR/bazel_dep_type/pkg && cd $TMPDIR/bazel_dep_type && \
48+
> echo "import pkg.dep; y: int = pkg.dep.x" > pkg/app.py && \
49+
> echo "x: str = 'typed'" > pkg/dep.py && \
50+
> printf '%s' '{"target":{"label":"//pkg:app","workspace_name":"_main","package":"pkg","name":"app","rule_kind":"py_binary"},"check_roots":{"sources":["pkg/app.py"]},"search_path":{"workspace_name":"_main","python_import_all_repositories":false},"config":{"python_version":"3.12","system_platform":"linux"}}' > input.json && \
51+
> $PYREFLY bazel-check --output out.json input.json > stdout.txt 2> stderr.txt; rc=$?; \
52+
> test ! -s stdout.txt && \
53+
> $JQ -r '[(.diagnostics | length), .diagnostics[0].path, .diagnostics[0].name] | join(" ")' out.json; \
54+
> exit $rc
55+
1 pkg/app.py bad-assignment
56+
[1]
57+
```
58+
59+
## Generated check root reports physical path
60+
61+
```scrut {output_stream: stdout}
62+
$ mkdir -p $TMPDIR/bazel_generated/bazel-out/darwin-fastbuild/bin && cd $TMPDIR/bazel_generated && \
63+
> echo "x: int = 'bad'" > bazel-out/darwin-fastbuild/bin/generated.py && \
64+
> printf '%s' '{"target":{"label":"//pkg:generated","workspace_name":"_main","package":"pkg","name":"generated","rule_kind":"py_library"},"check_roots":{"sources":["generated.py"]},"search_path":{"workspace_name":"_main","python_import_all_repositories":false},"path_overlays":[{"short_path":"generated.py","path":"bazel-out/darwin-fastbuild/bin/generated.py"}],"config":{"python_version":"3.12","system_platform":"linux"}}' > input.json && \
65+
> $PYREFLY bazel-check --output out.json input.json > stdout.txt 2> stderr.txt; rc=$?; \
66+
> test ! -s stdout.txt && \
67+
> cat stderr.txt && \
68+
> $JQ -r '[(.diagnostics | length), .diagnostics[0].path, .diagnostics[0].target] | join(" ")' out.json; \
69+
> exit $rc
70+
Target //pkg:generated
71+
ERROR * [bad-assignment] (glob)
72+
*bazel-out/darwin-fastbuild/bin/generated.py* (glob)
73+
* (glob+)
74+
1 bazel-out/darwin-fastbuild/bin/generated.py //pkg:generated
75+
[1]
76+
```
77+
78+
## Warning threshold controls output and exit status
79+
80+
```scrut {output_stream: stdout}
81+
$ mkdir -p $TMPDIR/bazel_warn/pkg && cd $TMPDIR/bazel_warn && \
82+
> echo "x: int = 'bad'" > pkg/app.py && \
83+
> printf '%s' '{"target":{"label":"//pkg:app","workspace_name":"_main","package":"pkg","name":"app","rule_kind":"py_binary"},"check_roots":{"sources":["pkg/app.py"]},"search_path":{"workspace_name":"_main","python_import_all_repositories":false},"config":{"python_version":"3.12","system_platform":"linux","error_severities":{"bad-assignment":"warn"}}}' > input.json && \
84+
> $PYREFLY bazel-check --output default.json input.json > default.stdout 2> default.stderr && \
85+
> test ! -s default.stdout && test ! -s default.stderr && \
86+
> $JQ '.diagnostics | length' default.json && \
87+
> $PYREFLY bazel-check --min-severity warn --output warn.json input.json > warn.stdout 2> warn.stderr; rc=$?; \
88+
> test ! -s warn.stdout && \
89+
> $JQ -r '[(.diagnostics | length), .diagnostics[0].name, .diagnostics[0].severity] | join(" ")' warn.json; \
90+
> exit $rc
91+
0
92+
1 bad-assignment warn
93+
[1]
94+
```
95+
96+
## Directive output survives the default severity threshold
97+
98+
```scrut {output_stream: stdout}
99+
$ mkdir -p $TMPDIR/bazel_reveal/pkg && cd $TMPDIR/bazel_reveal && \
100+
> echo "from typing import reveal_type; reveal_type(1)" > pkg/app.py && \
101+
> printf '%s' '{"target":{"label":"//pkg:app","workspace_name":"_main","package":"pkg","name":"app","rule_kind":"py_binary"},"check_roots":{"sources":["pkg/app.py"]},"search_path":{"workspace_name":"_main","python_import_all_repositories":false},"config":{"python_version":"3.12","system_platform":"linux"}}' > input.json && \
102+
> $PYREFLY bazel-check --output out.json input.json > stdout.txt 2> stderr.txt && \
103+
> test ! -s stdout.txt && \
104+
> cat stderr.txt && \
105+
> $JQ -r '[(.diagnostics | length), .diagnostics[0].path, .diagnostics[0].name] | join(" ")' out.json
106+
Target //pkg:app
107+
INFO revealed type: Literal[1] [reveal-type]
108+
*pkg/app.py* (glob)
109+
* (glob+)
110+
1 pkg/app.py reveal-type
111+
[0]
112+
```

0 commit comments

Comments
 (0)