fix(msvc): handle all per-warning cl.exe options

cl.exe accepts a warning-number argument either glued (/wd4995) or as
a separate token (/wd 4995); the latter form is widely used in nmake
Makefiles. Three gaps on the current 4.1.2-rc tip:

1. `/wd*`, `/we*`, `/wo*` used a plain prefix pattern that matches the
   glued form only. When bear saw "/wd 4995", the flag consumed zero
   extra args and the trailing numeric token was reclassified as a
   Source and dropped from compile_commands.json. clangd then emitted
   drv_invalid_int_value for every translation unit.

2. `/w1nnnn`, `/w2nnnn`, `/w3nnnn`, `/w4nnnn` (set warning level for a
   specific warning) were not defined at all, so `/w1 4326` was split
   into an unknown flag plus an orphan numeric token.

3. `/Wv[:version]` was not defined. Both the bare `/Wv` form (cl uses
   the current compiler version when omitted) and `/Wv:17` were
   affected.

All three classes are documented on the MS warning-level options page:
https://learn.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level

Fix:
  * /wd*, /we*, /wo*  ->  /wd{ }*, /we{ }*, /wo{ }*
    (ExactlyWithGluedOrSep, matching /D, /I, /U, /FI).
  * Add /w1{ }*, /w2{ }*, /w3{ }*, /w4{ }*.
  * Add /Wv (exact) plus /Wv:* (ExactlyWithColon, required value).

clang_cl.yaml inherits the fix via `extends: msvc`. Codegen snapshot
fixtures updated accordingly.

Two integration tests in integration-tests/tests/cases/semantic.rs
cover all three classes.

Manually verified by scc-tw <scc@scc.tw>.
Closes: #690

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
scc
2026-04-21 01:51:05 +08:00
committed by László Nagy
parent c11cf30ac4
commit 9c04b2e1a5
4 changed files with 178 additions and 11 deletions
@@ -1,9 +1,10 @@
---
source: bear-codegen/tests/snapshots.rs
assertion_line: 32
expression: "generate_flag_file(\"clang_cl\")"
---
// Generated from interpreters/clang_cl.yaml -- DO NOT EDIT
static CLANG_CL_FLAGS: [FlagRule; 163] = [
static CLANG_CL_FLAGS: [FlagRule; 169] = [
FlagRule::new(FlagPattern::ExactlyWithEqOrSep("-fms-compatibility-version"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
FlagRule::new(FlagPattern::Exactly("-fprofile-instr-generate", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
FlagRule::new(FlagPattern::ExactlyWithEq("-fprofile-instr-generate"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
@@ -139,9 +140,15 @@ static CLANG_CL_FLAGS: [FlagRule; 163] = [
FlagRule::new(FlagPattern::Exactly("/W2", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/W3", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/W4", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Prefix("/wd", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Prefix("/we", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Prefix("/wo", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithColon("/Wv"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/Wv", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w1"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w2"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w3"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w4"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wd"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/we"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wo"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/FC", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/Yc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/Yu"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
@@ -1,9 +1,10 @@
---
source: bear-codegen/tests/snapshots.rs
assertion_line: 62
expression: "generate_flag_file(\"msvc\")"
---
// Generated from interpreters/msvc.yaml -- DO NOT EDIT
static MSVC_FLAGS: [FlagRule; 127] = [
static MSVC_FLAGS: [FlagRule; 133] = [
FlagRule::new(FlagPattern::Exactly("/external:anglebrackets", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/Qfast_transcendentals", 0), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
FlagRule::new(FlagPattern::ExactlyWithColon("/execution-charset"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
@@ -104,9 +105,15 @@ static MSVC_FLAGS: [FlagRule; 127] = [
FlagRule::new(FlagPattern::Exactly("/W2", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/W3", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/W4", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Prefix("/wd", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Prefix("/we", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Prefix("/wo", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithColon("/Wv"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/Wv", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w1"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w2"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w3"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/w4"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wd"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/we"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithGluedOrSep("/wo"), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::Exactly("/FC", 0), ArgumentKind::Other(PassEffect::None)),
FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/Yc"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
FlagRule::new(FlagPattern::ExactlyWithColonOrSep("/Yu"), ArgumentKind::Other(PassEffect::Configures(CompilerPass::Compiling))),
+20 -3
View File
@@ -220,11 +220,28 @@ flags:
result: none
- match: {pattern: "/w"}
result: none
- match: {pattern: "/wd*"}
# /Wv[:version]: the value is optional (cl uses the current compiler version
# when omitted), so we accept both the bare form and "/Wv:<version>".
- match: {pattern: "/Wv:*"}
result: none
- match: {pattern: "/we*"}
- match: {pattern: "/Wv"}
result: none
- match: {pattern: "/wo*"}
# cl.exe accepts the warning-number value either glued ("/wd4995") or as a
# separate argument ("/wd 4995") for all per-warning options; the patterns
# below must consume both forms.
- match: {pattern: "/w1{ }*"}
result: none
- match: {pattern: "/w2{ }*"}
result: none
- match: {pattern: "/w3{ }*"}
result: none
- match: {pattern: "/w4{ }*"}
result: none
- match: {pattern: "/wd{ }*"}
result: none
- match: {pattern: "/we{ }*"}
result: none
- match: {pattern: "/wo{ }*"}
result: none
- match: {pattern: "/diagnostics:*"}
result: none
+136
View File
@@ -504,3 +504,139 @@ fn semantic_output_format() -> Result<()> {
Ok(())
}
/// Regression test: all MSVC per-warning options documented on
/// <https://learn.microsoft.com/en-us/cpp/build/reference/compiler-option-warning-level>
/// accept their numeric value either glued (`/wd4995`) or separated by whitespace
/// (`/wd 4995`). Both forms are emitted by real `cl.exe` invocations and by
/// Makefiles in the wild (e.g. `CFLAGS = /wd 4995 /wd 4996 ...`). The separated
/// form must survive semantic analysis intact; dropping the number silently would
/// corrupt compile_commands.json and break downstream tools such as clangd
/// (emits `drv_invalid_int_value` per translation unit).
///
/// Covers `/w1`, `/w2`, `/w3`, `/w4` (set warning level for a specific warning)
/// and `/wd`, `/we`, `/wo` (disable / as-error / report-once).
///
/// This test is platform-independent: it exercises the `semantic` subcommand on
/// a hand-crafted events file and does not require a real `cl.exe` to be present.
#[test]
fn msvc_per_warning_options_preserve_separated_value() -> Result<()> {
let env = TestEnvironment::new("msvc_per_warning_options_separated")?;
let temp_dir = env.test_dir().to_str().unwrap();
// Use a bare "cl.exe" -- the recognizer matches on the filename stem only, so we
// do not need the file to exist on disk. Keeps the test hermetic across platforms.
let cl = "cl.exe";
let event = json!({
"pid": 1,
"execution": {
"executable": cl,
"arguments": [
cl,
"/w1", "4100",
"/w2", "4101",
"/w3", "4102",
"/w4", "4103",
"/wd", "4995",
"/we", "4996",
"/wo", "4819",
"/c", "test.c",
],
"working_dir": temp_dir,
"environment": {}
}
});
env.create_source_files(&[
("events.json", &event.to_string()),
("test.c", "int main(void) { return 0; }"),
])?;
env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?;
let db = env.load_compilation_database("compile_commands.json")?;
db.assert_count(1)?;
// Each flag/value pair must round-trip with its numeric value intact. Before
// the fix, these flags matched a prefix-only pattern, so the standalone
// numeric token following each flag was reclassified as a source file and
// dropped from the output.
db.assert_contains(&compilation_entry!(
file: "test.c".to_string(),
directory: temp_dir.to_string(),
arguments: vec![
cl.to_string(),
"/w1".to_string(), "4100".to_string(),
"/w2".to_string(), "4101".to_string(),
"/w3".to_string(), "4102".to_string(),
"/w4".to_string(), "4103".to_string(),
"/wd".to_string(), "4995".to_string(),
"/we".to_string(), "4996".to_string(),
"/wo".to_string(), "4819".to_string(),
"/c".to_string(),
"test.c".to_string(),
]
))?;
Ok(())
}
/// Regression test: `/Wv[:version]` has an optional value (cl uses the current
/// compiler version when omitted). Both forms -- bare `/Wv` and `/Wv:17` -- must
/// round-trip through semantic analysis without losing tokens or dropping the
/// entry.
#[test]
fn msvc_wv_optional_version_is_preserved() -> Result<()> {
let env = TestEnvironment::new("msvc_wv_optional_version")?;
let temp_dir = env.test_dir().to_str().unwrap();
let cl = "cl.exe";
// Two translation units, one per /Wv form, so the test exercises both paths
// in a single run.
let event_bare = json!({
"pid": 1,
"execution": {
"executable": cl,
"arguments": [cl, "/Wv", "/c", "bare.c"],
"working_dir": temp_dir,
"environment": {}
}
});
let event_with_version = json!({
"pid": 2,
"execution": {
"executable": cl,
"arguments": [cl, "/Wv:17", "/c", "versioned.c"],
"working_dir": temp_dir,
"environment": {}
}
});
let events = format!("{}\n{}", event_bare, event_with_version);
env.create_source_files(&[
("events.json", &events),
("bare.c", "int main(void) { return 0; }"),
("versioned.c", "int main(void) { return 0; }"),
])?;
env.run_bear_success(&["semantic", "--input", "events.json", "--output", "compile_commands.json"])?;
let db = env.load_compilation_database("compile_commands.json")?;
db.assert_count(2)?;
db.assert_contains(&compilation_entry!(
file: "bare.c".to_string(),
directory: temp_dir.to_string(),
arguments: vec![cl.to_string(), "/Wv".to_string(), "/c".to_string(), "bare.c".to_string()]
))?;
db.assert_contains(&compilation_entry!(
file: "versioned.c".to_string(),
directory: temp_dir.to_string(),
arguments: vec![cl.to_string(), "/Wv:17".to_string(), "/c".to_string(), "versioned.c".to_string()]
))?;
Ok(())
}