mirror of
https://github.com/openai/codex.git
synced 2026-05-02 02:17:22 +00:00
Compare commits
2 Commits
dev/rasmus
...
joshka/pr1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
300d93ed36 | ||
|
|
4e594bc93b |
@@ -11,6 +11,10 @@ In the codex-rs folder where the rust code lives:
|
||||
- Always collapse if statements per https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
|
||||
- Always inline format! args when possible per https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args
|
||||
- Use method references over closures when possible per https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure_for_method_calls
|
||||
- Follow the `argument_comment_lint` convention for anonymous literal arguments in Rust call sites:
|
||||
- Use an exact `/*param_name=*/` comment before opaque literal arguments such as `None`, booleans, and numeric literals when passing them by position.
|
||||
- Do not add these comments for string or char literals unless the comment adds real clarity; those literals are intentionally exempt from the lint.
|
||||
- If you add one of these comments, the parameter name must exactly match the callee signature.
|
||||
- When possible, make `match` statements exhaustive and avoid wildcard arms.
|
||||
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
|
||||
- When making a change that adds or changes an API, ensure that the documentation in the `docs/` folder is up to date if applicable.
|
||||
|
||||
@@ -841,7 +841,7 @@ async fn enable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyhow
|
||||
let codex_home = find_codex_home()?;
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.with_profile(interactive.config_profile.as_deref())
|
||||
.set_feature_enabled(feature, true)
|
||||
.enable_feature(feature)
|
||||
.apply()
|
||||
.await?;
|
||||
println!("Enabled feature `{feature}` in config.toml.");
|
||||
@@ -854,7 +854,7 @@ async fn disable_feature_in_config(interactive: &TuiCli, feature: &str) -> anyho
|
||||
let codex_home = find_codex_home()?;
|
||||
ConfigEditsBuilder::new(&codex_home)
|
||||
.with_profile(interactive.config_profile.as_deref())
|
||||
.set_feature_enabled(feature, false)
|
||||
.disable_feature(feature)
|
||||
.apply()
|
||||
.await?;
|
||||
println!("Disabled feature `{feature}` in config.toml.");
|
||||
|
||||
@@ -857,11 +857,20 @@ impl ConfigEditsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable or disable a feature flag by key under the `[features]` table.
|
||||
pub fn set_feature_enabled(mut self, key: &str, enabled: bool) -> Self {
|
||||
/// Enable a feature flag by key under the `[features]` table.
|
||||
pub fn enable_feature(mut self, key: &str) -> Self {
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments: vec!["features".to_string(), key.to_string()],
|
||||
value: value(enabled),
|
||||
value: value(true),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable a feature flag by key under the `[features]` table.
|
||||
pub fn disable_feature(mut self, key: &str) -> Self {
|
||||
self.edits.push(ConfigEdit::SetPath {
|
||||
segments: vec!["features".to_string(), key.to_string()],
|
||||
value: value(false),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
@@ -102,7 +102,11 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
|
||||
let command = crate::tools::handlers::unified_exec::get_command(
|
||||
¶ms,
|
||||
invocation.session.user_shell(),
|
||||
invocation.turn.tools_config.allow_login_shell,
|
||||
if invocation.turn.tools_config.allow_login_shell {
|
||||
crate::tools::handlers::unified_exec::LoginShellPolicy::Allow
|
||||
} else {
|
||||
crate::tools::handlers::unified_exec::LoginShellPolicy::Disallow
|
||||
},
|
||||
)
|
||||
.ok()?;
|
||||
Some((command, invocation.turn.resolve_path(params.workdir)))
|
||||
|
||||
@@ -107,7 +107,11 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
let command = match get_command(
|
||||
¶ms,
|
||||
invocation.session.user_shell(),
|
||||
invocation.turn.tools_config.allow_login_shell,
|
||||
if invocation.turn.tools_config.allow_login_shell {
|
||||
LoginShellPolicy::Allow
|
||||
} else {
|
||||
LoginShellPolicy::Disallow
|
||||
},
|
||||
) {
|
||||
Ok(command) => command,
|
||||
Err(_) => return true,
|
||||
@@ -154,7 +158,11 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
let command = get_command(
|
||||
&args,
|
||||
session.user_shell(),
|
||||
turn.tools_config.allow_login_shell,
|
||||
if turn.tools_config.allow_login_shell {
|
||||
LoginShellPolicy::Allow
|
||||
} else {
|
||||
LoginShellPolicy::Disallow
|
||||
},
|
||||
)
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
|
||||
@@ -317,10 +325,22 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum LoginShellPolicy {
|
||||
Allow,
|
||||
Disallow,
|
||||
}
|
||||
|
||||
impl LoginShellPolicy {
|
||||
fn allows_login_shell(self) -> bool {
|
||||
matches!(self, Self::Allow)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_command(
|
||||
args: &ExecCommandArgs,
|
||||
session_shell: Arc<Shell>,
|
||||
allow_login_shell: bool,
|
||||
login_shell_policy: LoginShellPolicy,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let model_shell = args.shell.as_ref().map(|shell_str| {
|
||||
let mut shell = get_shell_by_model_provided_path(&PathBuf::from(shell_str));
|
||||
@@ -329,6 +349,7 @@ pub(crate) fn get_command(
|
||||
});
|
||||
|
||||
let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref());
|
||||
let allow_login_shell = login_shell_policy.allows_login_shell();
|
||||
let use_login_shell = match args.login {
|
||||
Some(true) if !allow_login_shell => {
|
||||
return Err(
|
||||
|
||||
@@ -18,8 +18,12 @@ fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()>
|
||||
|
||||
assert!(args.shell.is_none());
|
||||
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
let command = get_command(
|
||||
&args,
|
||||
Arc::new(default_user_shell()),
|
||||
LoginShellPolicy::Allow,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command.len(), 3);
|
||||
assert_eq!(command[2], "echo hello");
|
||||
@@ -34,8 +38,12 @@ fn test_get_command_respects_explicit_bash_shell() -> anyhow::Result<()> {
|
||||
|
||||
assert_eq!(args.shell.as_deref(), Some("/bin/bash"));
|
||||
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
let command = get_command(
|
||||
&args,
|
||||
Arc::new(default_user_shell()),
|
||||
LoginShellPolicy::Allow,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command.last(), Some(&"echo hello".to_string()));
|
||||
if command
|
||||
@@ -55,8 +63,12 @@ fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> {
|
||||
|
||||
assert_eq!(args.shell.as_deref(), Some("powershell"));
|
||||
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
let command = get_command(
|
||||
&args,
|
||||
Arc::new(default_user_shell()),
|
||||
LoginShellPolicy::Allow,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command[2], "echo hello");
|
||||
Ok(())
|
||||
@@ -70,8 +82,12 @@ fn test_get_command_respects_explicit_cmd_shell() -> anyhow::Result<()> {
|
||||
|
||||
assert_eq!(args.shell.as_deref(), Some("cmd"));
|
||||
|
||||
let command =
|
||||
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
|
||||
let command = get_command(
|
||||
&args,
|
||||
Arc::new(default_user_shell()),
|
||||
LoginShellPolicy::Allow,
|
||||
)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
assert_eq!(command[2], "echo hello");
|
||||
Ok(())
|
||||
@@ -82,8 +98,12 @@ fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<(
|
||||
let json = r#"{"cmd": "echo hello", "login": true}"#;
|
||||
|
||||
let args: ExecCommandArgs = parse_arguments(json)?;
|
||||
let err = get_command(&args, Arc::new(default_user_shell()), false)
|
||||
.expect_err("explicit login should be rejected");
|
||||
let err = get_command(
|
||||
&args,
|
||||
Arc::new(default_user_shell()),
|
||||
LoginShellPolicy::Disallow,
|
||||
)
|
||||
.expect_err("explicit login should be rejected");
|
||||
|
||||
assert!(
|
||||
err.contains("login shell is disabled by config"),
|
||||
|
||||
@@ -860,12 +860,15 @@ impl App {
|
||||
continue;
|
||||
}
|
||||
let effective_enabled = self.config.features.enabled(feature);
|
||||
self.chat_widget
|
||||
.set_feature_enabled(feature, effective_enabled);
|
||||
if effective_enabled {
|
||||
builder = builder.set_feature_enabled(feature_key, true);
|
||||
self.chat_widget.enable_feature(feature);
|
||||
} else {
|
||||
self.chat_widget.disable_feature(feature);
|
||||
}
|
||||
if effective_enabled {
|
||||
builder = builder.enable_feature(feature_key);
|
||||
} else if feature.default_enabled() {
|
||||
builder = builder.set_feature_enabled(feature_key, false);
|
||||
builder = builder.disable_feature(feature_key);
|
||||
} else {
|
||||
// If the feature already default to `false`, we drop the key
|
||||
// in the config file so that the user does not miss the feature
|
||||
@@ -5156,8 +5159,7 @@ mod tests {
|
||||
app.config
|
||||
.features
|
||||
.set_enabled(Feature::GuardianApproval, true)?;
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::GuardianApproval, true);
|
||||
app.chat_widget.enable_feature(Feature::GuardianApproval);
|
||||
let current_session_policy = app.config.permissions.approval_policy.value();
|
||||
|
||||
app.update_feature_flags(vec![(Feature::GuardianApproval, false)])
|
||||
|
||||
@@ -7649,6 +7649,14 @@ impl ChatWidget {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn enable_feature(&mut self, feature: Feature) -> bool {
|
||||
self.set_feature_enabled(feature, true)
|
||||
}
|
||||
|
||||
pub(crate) fn disable_feature(&mut self, feature: Feature) -> bool {
|
||||
self.set_feature_enabled(feature, false)
|
||||
}
|
||||
|
||||
fn initial_collaboration_mask(
|
||||
_config: &Config,
|
||||
models_manager: &ModelsManager,
|
||||
|
||||
5
justfile
5
justfile
@@ -86,6 +86,11 @@ write-app-server-schema *args:
|
||||
write-hooks-schema:
|
||||
cargo run --manifest-path ./codex-rs/Cargo.toml -p codex-hooks --bin write_hooks_schema_fixtures
|
||||
|
||||
# Run the argument-comment Dylint checks across codex-rs.
|
||||
[no-cd]
|
||||
argument-comment-lint *args:
|
||||
./tools/argument-comment-lint/run.sh "$@"
|
||||
|
||||
# Tail logs from the state SQLite database
|
||||
log *args:
|
||||
if [ "${1:-}" = "--" ]; then shift; fi; cargo run -p codex-state --bin logs_client -- "$@"
|
||||
|
||||
6
tools/argument-comment-lint/.cargo/config.toml
Normal file
6
tools/argument-comment-lint/.cargo/config.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[target.'cfg(all())']
|
||||
rustflags = ["-C", "linker=dylint-link"]
|
||||
|
||||
# For Rust versions 1.74.0 and onward, the following alternative can be used
|
||||
# (see https://github.com/rust-lang/cargo/pull/12535):
|
||||
# linker = "dylint-link"
|
||||
1
tools/argument-comment-lint/.gitignore
vendored
Normal file
1
tools/argument-comment-lint/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
1653
tools/argument-comment-lint/Cargo.lock
generated
Normal file
1653
tools/argument-comment-lint/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
tools/argument-comment-lint/Cargo.toml
Normal file
21
tools/argument-comment-lint/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "argument_comment_lint"
|
||||
version = "0.1.0"
|
||||
description = "Dylint lints for Rust /*param=*/ argument comments"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "20ce69b9a63bcd2756cd906fe0964d1e901e042a" }
|
||||
dylint_linting = "5.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
dylint_testing = "5.0.0"
|
||||
|
||||
[workspace]
|
||||
|
||||
[package.metadata.rust-analyzer]
|
||||
rustc_private = true
|
||||
113
tools/argument-comment-lint/README.md
Normal file
113
tools/argument-comment-lint/README.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# argument-comment-lint
|
||||
|
||||
Isolated [Dylint](https://github.com/trailofbits/dylint) library for enforcing
|
||||
Rust argument comments in the exact `/*param=*/` shape.
|
||||
|
||||
It provides two lints:
|
||||
|
||||
- `argument_comment_mismatch` (`warn` by default): validates that a present
|
||||
`/*param=*/` comment matches the resolved callee parameter name.
|
||||
- `uncommented_anonymous_literal_argument` (`allow` by default): flags
|
||||
anonymous literal-like arguments such as `None`, `true`, `false`, and numeric
|
||||
literals when they do not have a preceding `/*param=*/` comment.
|
||||
|
||||
String and char literals are exempt because they are often already
|
||||
self-descriptive at the callsite.
|
||||
|
||||
## Behavior
|
||||
|
||||
When you own the API, prefer a clearer shape over positional literal arguments:
|
||||
|
||||
```rust
|
||||
enum BaseUrl {
|
||||
Default,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
struct RetryCount(usize);
|
||||
|
||||
fn create_openai_url(base_url: BaseUrl, retry_count: RetryCount) -> String {
|
||||
let _ = (base_url, retry_count);
|
||||
String::new()
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
create_openai_url(BaseUrl::Default, RetryCount(3));
|
||||
```
|
||||
|
||||
When a minimal refactor needs to keep a legacy signature, `/*param=*/` comments
|
||||
make those call sites readable:
|
||||
|
||||
```rust
|
||||
fn legacy_create_openai_url(base_url: Option<String>, retry_count: usize) -> String {
|
||||
let _ = (base_url, retry_count);
|
||||
String::new()
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
legacy_create_openai_url(/*base_url=*/ None, /*retry_count=*/ 3);
|
||||
```
|
||||
|
||||
This is warned on by `argument_comment_mismatch`:
|
||||
|
||||
```rust
|
||||
legacy_create_openai_url(/*api_base=*/ None, 3);
|
||||
```
|
||||
|
||||
This is only warned on when `uncommented_anonymous_literal_argument` is enabled:
|
||||
|
||||
```rust
|
||||
legacy_create_openai_url(None, 3);
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Install the required tooling once:
|
||||
|
||||
```bash
|
||||
cargo install cargo-dylint dylint-link
|
||||
rustup toolchain install nightly-2025-09-18 \
|
||||
--component llvm-tools-preview \
|
||||
--component rustc-dev \
|
||||
--component rust-src
|
||||
```
|
||||
|
||||
Run the lint crate tests:
|
||||
|
||||
```bash
|
||||
cd tools/argument-comment-lint
|
||||
cargo test
|
||||
```
|
||||
|
||||
Run the lint against `codex-rs` from the repo root:
|
||||
|
||||
```bash
|
||||
./tools/argument-comment-lint/run.sh -p codex-core
|
||||
just argument-comment-lint -p codex-core
|
||||
```
|
||||
|
||||
If no package selection is provided, `run.sh` defaults to checking the
|
||||
`codex-rs` workspace with `--workspace --no-deps`.
|
||||
|
||||
Repo runs also promote `uncommented_anonymous_literal_argument` to an error by
|
||||
default:
|
||||
|
||||
```bash
|
||||
./tools/argument-comment-lint/run.sh -p codex-core
|
||||
```
|
||||
|
||||
The wrapper does that by setting `DYLINT_RUSTFLAGS`, and it leaves an explicit
|
||||
existing setting alone. To override that behavior for an ad hoc run:
|
||||
|
||||
```bash
|
||||
DYLINT_RUSTFLAGS="-A uncommented_anonymous_literal_argument" \
|
||||
./tools/argument-comment-lint/run.sh -p codex-core
|
||||
```
|
||||
|
||||
To expand target coverage for an ad hoc run:
|
||||
|
||||
```bash
|
||||
./tools/argument-comment-lint/run.sh -p codex-core -- --all-targets
|
||||
```
|
||||
87
tools/argument-comment-lint/run.sh
Executable file
87
tools/argument-comment-lint/run.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
lint_path="$repo_root/tools/argument-comment-lint"
|
||||
manifest_path="$repo_root/codex-rs/Cargo.toml"
|
||||
strict_lint="uncommented_anonymous_literal_argument"
|
||||
noise_lint="unknown_lints"
|
||||
|
||||
has_manifest_path=false
|
||||
has_package_selection=false
|
||||
has_no_deps=false
|
||||
has_library_selection=false
|
||||
expect_value=""
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ -n "$expect_value" ]]; then
|
||||
case "$expect_value" in
|
||||
manifest_path)
|
||||
has_manifest_path=true
|
||||
;;
|
||||
package_selection)
|
||||
has_package_selection=true
|
||||
;;
|
||||
library_selection)
|
||||
has_library_selection=true
|
||||
;;
|
||||
esac
|
||||
expect_value=""
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$arg" in
|
||||
--)
|
||||
break
|
||||
;;
|
||||
--manifest-path)
|
||||
expect_value="manifest_path"
|
||||
;;
|
||||
--manifest-path=*)
|
||||
has_manifest_path=true
|
||||
;;
|
||||
-p|--package)
|
||||
expect_value="package_selection"
|
||||
;;
|
||||
--package=*)
|
||||
has_package_selection=true
|
||||
;;
|
||||
--workspace)
|
||||
has_package_selection=true
|
||||
;;
|
||||
--no-deps)
|
||||
has_no_deps=true
|
||||
;;
|
||||
--lib|--lib-path)
|
||||
expect_value="library_selection"
|
||||
;;
|
||||
--lib=*|--lib-path=*)
|
||||
has_library_selection=true
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
cmd=(cargo dylint --path "$lint_path")
|
||||
if [[ "$has_library_selection" == false ]]; then
|
||||
cmd+=(--all)
|
||||
fi
|
||||
if [[ "$has_manifest_path" == false ]]; then
|
||||
cmd+=(--manifest-path "$manifest_path")
|
||||
fi
|
||||
if [[ "$has_package_selection" == false ]]; then
|
||||
cmd+=(--workspace)
|
||||
fi
|
||||
if [[ "$has_no_deps" == false ]]; then
|
||||
cmd+=(--no-deps)
|
||||
fi
|
||||
cmd+=("$@")
|
||||
|
||||
if [[ "${DYLINT_RUSTFLAGS:-}" != *"$strict_lint"* ]]; then
|
||||
export DYLINT_RUSTFLAGS="${DYLINT_RUSTFLAGS:+${DYLINT_RUSTFLAGS} }-D $strict_lint"
|
||||
fi
|
||||
if [[ "${DYLINT_RUSTFLAGS:-}" != *"$noise_lint"* ]]; then
|
||||
export DYLINT_RUSTFLAGS="${DYLINT_RUSTFLAGS:+${DYLINT_RUSTFLAGS} }-A $noise_lint"
|
||||
fi
|
||||
|
||||
exec "${cmd[@]}"
|
||||
3
tools/argument-comment-lint/rust-toolchain
Normal file
3
tools/argument-comment-lint/rust-toolchain
Normal file
@@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2025-09-18"
|
||||
components = ["llvm-tools-preview", "rustc-dev", "rust-src"]
|
||||
61
tools/argument-comment-lint/src/comment_parser.rs
Normal file
61
tools/argument-comment-lint/src/comment_parser.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
pub fn parse_argument_comment(text: &str) -> Option<&str> {
|
||||
let trimmed = text.trim_end();
|
||||
let comment_start = trimmed.rfind("/*")?;
|
||||
let comment = &trimmed[comment_start..];
|
||||
let name = comment.strip_prefix("/*")?.strip_suffix("=*/")?;
|
||||
is_identifier(name).then_some(name)
|
||||
}
|
||||
|
||||
pub fn parse_argument_comment_prefix(text: &str) -> Option<&str> {
|
||||
let trimmed = text.trim_start();
|
||||
let comment = trimmed.strip_prefix("/*")?;
|
||||
let (name, _) = comment.split_once("=*/")?;
|
||||
is_identifier(name).then_some(name)
|
||||
}
|
||||
|
||||
fn is_identifier(text: &str) -> bool {
|
||||
let mut chars = text.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
return false;
|
||||
};
|
||||
if !(first == '_' || first.is_ascii_alphabetic()) {
|
||||
return false;
|
||||
}
|
||||
chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_argument_comment;
|
||||
use super::parse_argument_comment_prefix;
|
||||
|
||||
#[test]
|
||||
fn parses_trailing_comment() {
|
||||
assert_eq!(parse_argument_comment("(/*base_url=*/ "), Some("base_url"));
|
||||
assert_eq!(
|
||||
parse_argument_comment(", /*timeout_ms=*/ "),
|
||||
Some("timeout_ms")
|
||||
);
|
||||
assert_eq!(
|
||||
parse_argument_comment(".method::<u8>(/*base_url=*/ "),
|
||||
Some("base_url")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_matching_shapes() {
|
||||
assert_eq!(parse_argument_comment("(\n"), None);
|
||||
assert_eq!(parse_argument_comment("(/* base_url=*/ "), None);
|
||||
assert_eq!(parse_argument_comment("(/*base_url =*/ "), None);
|
||||
assert_eq!(parse_argument_comment("(/*1base_url=*/ "), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_prefix_comment() {
|
||||
assert_eq!(parse_argument_comment_prefix("/*env=*/ None"), Some("env"));
|
||||
assert_eq!(
|
||||
parse_argument_comment_prefix("\n /*retry_count=*/ 3"),
|
||||
Some("retry_count")
|
||||
);
|
||||
}
|
||||
}
|
||||
279
tools/argument-comment-lint/src/lib.rs
Normal file
279
tools/argument-comment-lint/src/lib.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
#![feature(rustc_private)]
|
||||
|
||||
mod comment_parser;
|
||||
|
||||
extern crate rustc_ast;
|
||||
extern crate rustc_errors;
|
||||
extern crate rustc_hir;
|
||||
extern crate rustc_lint;
|
||||
extern crate rustc_middle;
|
||||
extern crate rustc_session;
|
||||
extern crate rustc_span;
|
||||
|
||||
use clippy_utils::diagnostics::span_lint_and_help;
|
||||
use clippy_utils::diagnostics::span_lint_and_sugg;
|
||||
use clippy_utils::fn_def_id;
|
||||
use clippy_utils::is_res_lang_ctor;
|
||||
use clippy_utils::peel_blocks;
|
||||
use clippy_utils::source::snippet;
|
||||
use rustc_ast::LitKind;
|
||||
use rustc_errors::Applicability;
|
||||
use rustc_hir::Expr;
|
||||
use rustc_hir::ExprKind;
|
||||
use rustc_hir::LangItem;
|
||||
use rustc_hir::UnOp;
|
||||
use rustc_hir::def::DefKind;
|
||||
use rustc_lint::LateContext;
|
||||
use rustc_lint::LateLintPass;
|
||||
use rustc_span::BytePos;
|
||||
use rustc_span::Span;
|
||||
|
||||
use crate::comment_parser::parse_argument_comment;
|
||||
use crate::comment_parser::parse_argument_comment_prefix;
|
||||
|
||||
dylint_linting::dylint_library!();
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub fn register_lints(_sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
|
||||
lint_store.register_lints(&[
|
||||
ARGUMENT_COMMENT_MISMATCH,
|
||||
UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT,
|
||||
]);
|
||||
lint_store.register_late_pass(|_| Box::new(ArgumentCommentLint));
|
||||
}
|
||||
|
||||
rustc_session::declare_lint! {
|
||||
/// ### What it does
|
||||
///
|
||||
/// Checks `/*param=*/` argument comments and verifies that the comment
|
||||
/// matches the resolved callee parameter name.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// A mismatched comment is worse than no comment because it actively
|
||||
/// misleads the reader.
|
||||
///
|
||||
/// ### Known problems
|
||||
///
|
||||
/// This lint only runs when the callee resolves to a concrete function or
|
||||
/// method with available parameter names.
|
||||
///
|
||||
/// If you own the API, prefer a clearer signature such as enums, named
|
||||
/// methods, or newtypes. This lint is primarily for legacy signatures
|
||||
/// where a minimal refactor is the better tradeoff.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```rust
|
||||
/// fn legacy_create_openai_url(base_url: Option<String>) -> String {
|
||||
/// String::new()
|
||||
/// }
|
||||
///
|
||||
/// legacy_create_openai_url(/*api_base=*/ None);
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```rust
|
||||
/// fn legacy_create_openai_url(base_url: Option<String>) -> String {
|
||||
/// String::new()
|
||||
/// }
|
||||
///
|
||||
/// legacy_create_openai_url(/*base_url=*/ None);
|
||||
/// ```
|
||||
pub ARGUMENT_COMMENT_MISMATCH,
|
||||
Warn,
|
||||
"argument comment does not match the resolved parameter name"
|
||||
}
|
||||
|
||||
rustc_session::declare_lint! {
|
||||
/// ### What it does
|
||||
///
|
||||
/// Requires a `/*param=*/` comment before anonymous literal-like
|
||||
/// arguments such as `None`, booleans, and numeric literals.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// Bare literal-like arguments make call sites harder to read because the
|
||||
/// meaning of the value is hidden in the callee signature.
|
||||
///
|
||||
/// ### Known problems
|
||||
///
|
||||
/// This lint is opinionated, so it is `allow` by default.
|
||||
/// Prefer reshaping APIs you control instead of relying on comments for
|
||||
/// new code; this lint exists for legacy call sites and minimal refactors.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```rust
|
||||
/// fn legacy_create_openai_url(base_url: Option<String>) -> String {
|
||||
/// String::new()
|
||||
/// }
|
||||
///
|
||||
/// legacy_create_openai_url(None);
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```rust
|
||||
/// fn legacy_create_openai_url(base_url: Option<String>) -> String {
|
||||
/// String::new()
|
||||
/// }
|
||||
///
|
||||
/// legacy_create_openai_url(/*base_url=*/ None);
|
||||
/// ```
|
||||
pub UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT,
|
||||
Allow,
|
||||
"anonymous literal-like argument is missing a `/*param=*/` comment"
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ArgumentCommentLint;
|
||||
|
||||
rustc_session::impl_lint_pass!(
|
||||
ArgumentCommentLint => [ARGUMENT_COMMENT_MISMATCH, UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT]
|
||||
);
|
||||
|
||||
impl<'tcx> LateLintPass<'tcx> for ArgumentCommentLint {
|
||||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
|
||||
if expr.span.from_expansion() {
|
||||
return;
|
||||
}
|
||||
|
||||
match expr.kind {
|
||||
ExprKind::Call(callee, args) => {
|
||||
self.check_call(cx, expr, callee.span, args, 0);
|
||||
}
|
||||
ExprKind::MethodCall(_, receiver, args, _) => {
|
||||
self.check_call(cx, expr, receiver.span, args, 1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgumentCommentLint {
|
||||
fn check_call<'tcx>(
|
||||
&self,
|
||||
cx: &LateContext<'tcx>,
|
||||
call: &'tcx Expr<'tcx>,
|
||||
first_gap_anchor: Span,
|
||||
args: &'tcx [Expr<'tcx>],
|
||||
parameter_offset: usize,
|
||||
) {
|
||||
let Some(def_id) = fn_def_id(cx, call) else {
|
||||
return;
|
||||
};
|
||||
if !def_id.is_local() && !is_workspace_crate_name(cx.tcx.crate_name(def_id.krate).as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !matches!(cx.tcx.def_kind(def_id), DefKind::Fn | DefKind::AssocFn) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parameter_names: Vec<_> = cx.tcx.fn_arg_idents(def_id).iter().copied().collect();
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
if arg.span.from_expansion() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(expected_name) = parameter_names.get(index + parameter_offset) else {
|
||||
continue;
|
||||
};
|
||||
let Some(expected_name) = expected_name else {
|
||||
continue;
|
||||
};
|
||||
let expected_name = expected_name.name.to_string();
|
||||
if !is_meaningful_parameter_name(&expected_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let boundary_span = if index == 0 {
|
||||
first_gap_anchor
|
||||
} else {
|
||||
args[index - 1].span
|
||||
};
|
||||
let gap_span = boundary_span.between(arg.span);
|
||||
let gap_text = snippet(cx, gap_span, "");
|
||||
let arg_text = snippet(cx, arg.span, "..");
|
||||
let lookbehind_start = BytePos(arg.span.lo().0.saturating_sub(64));
|
||||
let lookbehind_text =
|
||||
snippet(cx, arg.span.shrink_to_lo().with_lo(lookbehind_start), "");
|
||||
let argument_comment = parse_argument_comment(gap_text.as_ref())
|
||||
.or_else(|| parse_argument_comment(lookbehind_text.as_ref()))
|
||||
.or_else(|| parse_argument_comment_prefix(arg_text.as_ref()));
|
||||
|
||||
if let Some(actual_name) = argument_comment {
|
||||
if actual_name != expected_name {
|
||||
span_lint_and_help(
|
||||
cx,
|
||||
ARGUMENT_COMMENT_MISMATCH,
|
||||
arg.span,
|
||||
format!(
|
||||
"argument comment `/*{actual_name}=*/` does not match parameter `{expected_name}`"
|
||||
),
|
||||
None,
|
||||
format!("use `/*{expected_name}=*/`"),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !is_anonymous_literal_like(cx, arg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
span_lint_and_sugg(
|
||||
cx,
|
||||
UNCOMMENTED_ANONYMOUS_LITERAL_ARGUMENT,
|
||||
arg.span,
|
||||
format!("anonymous literal-like argument for parameter `{expected_name}`"),
|
||||
"prepend the parameter name comment",
|
||||
format!("/*{expected_name}=*/ {arg_text}"),
|
||||
Applicability::MachineApplicable,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_anonymous_literal_like(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
|
||||
let expr = peel_blocks(expr);
|
||||
match expr.kind {
|
||||
ExprKind::Lit(lit) => !matches!(
|
||||
lit.node,
|
||||
LitKind::Str(..) | LitKind::ByteStr(..) | LitKind::CStr(..) | LitKind::Char(..)
|
||||
),
|
||||
ExprKind::Unary(UnOp::Neg, inner) => matches!(peel_blocks(inner).kind, ExprKind::Lit(_)),
|
||||
ExprKind::Path(qpath) => {
|
||||
is_res_lang_ctor(cx, cx.qpath_res(&qpath, expr.hir_id), LangItem::OptionNone)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_meaningful_parameter_name(name: &str) -> bool {
|
||||
!name.is_empty() && !name.starts_with('_')
|
||||
}
|
||||
|
||||
fn is_workspace_crate_name(name: &str) -> bool {
|
||||
name.starts_with("codex_")
|
||||
|| matches!(
|
||||
name,
|
||||
"app_test_support" | "core_test_support" | "mcp_test_support"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui() {
|
||||
dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_crate_filter_accepts_first_party_names_only() {
|
||||
assert!(is_workspace_crate_name("codex_core"));
|
||||
assert!(is_workspace_crate_name("codex_tui"));
|
||||
assert!(is_workspace_crate_name("core_test_support"));
|
||||
assert!(!is_workspace_crate_name("std"));
|
||||
assert!(!is_workspace_crate_name("tokio"));
|
||||
}
|
||||
9
tools/argument-comment-lint/ui/allow_char_literals.rs
Normal file
9
tools/argument-comment-lint/ui/allow_char_literals.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#![warn(uncommented_anonymous_literal_argument)]
|
||||
|
||||
fn split_top_level(body: &str, delimiter: char) {
|
||||
let _ = (body, delimiter);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
split_top_level("a|b|c", '|');
|
||||
}
|
||||
9
tools/argument-comment-lint/ui/allow_string_literals.rs
Normal file
9
tools/argument-comment-lint/ui/allow_string_literals.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#![warn(uncommented_anonymous_literal_argument)]
|
||||
|
||||
fn describe(prefix: &str, suffix: &str) {
|
||||
let _ = (prefix, suffix);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
describe("openai", r"https://api.openai.com/v1");
|
||||
}
|
||||
12
tools/argument-comment-lint/ui/comment_matches.rs
Normal file
12
tools/argument-comment-lint/ui/comment_matches.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
#![warn(argument_comment_mismatch)]
|
||||
|
||||
fn legacy_create_openai_url(base_url: Option<String>, retry_count: usize) -> String {
|
||||
let _ = (base_url, retry_count);
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let base_url = Some(String::from("https://api.openai.com"));
|
||||
legacy_create_openai_url(base_url, 3);
|
||||
legacy_create_openai_url(/*base_url=*/ None, 3);
|
||||
}
|
||||
15
tools/argument-comment-lint/ui/comment_matches_multiline.rs
Normal file
15
tools/argument-comment-lint/ui/comment_matches_multiline.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
#![warn(argument_comment_mismatch)]
|
||||
#![warn(uncommented_anonymous_literal_argument)]
|
||||
|
||||
fn legacy_run_git_for_stdout(repo_root: &str, args: Vec<&str>, env: Option<&str>) -> String {
|
||||
let _ = (repo_root, args, env);
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = legacy_run_git_for_stdout(
|
||||
"/tmp/repo",
|
||||
vec!["rev-parse", "HEAD"],
|
||||
/*env=*/ None,
|
||||
);
|
||||
}
|
||||
10
tools/argument-comment-lint/ui/comment_mismatch.rs
Normal file
10
tools/argument-comment-lint/ui/comment_mismatch.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
#![warn(argument_comment_mismatch)]
|
||||
|
||||
fn legacy_create_openai_url(base_url: Option<String>) -> String {
|
||||
let _ = base_url;
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = legacy_create_openai_url(/*api_base=*/ None);
|
||||
}
|
||||
15
tools/argument-comment-lint/ui/comment_mismatch.stderr
Normal file
15
tools/argument-comment-lint/ui/comment_mismatch.stderr
Normal file
@@ -0,0 +1,15 @@
|
||||
warning: argument comment `/*api_base=*/` does not match parameter `base_url`
|
||||
--> $DIR/comment_mismatch.rs:9:52
|
||||
|
|
||||
LL | let _ = legacy_create_openai_url(/*api_base=*/ None);
|
||||
| ^^^^
|
||||
|
|
||||
= help: use `/*base_url=*/`
|
||||
note: the lint level is defined here
|
||||
--> $DIR/comment_mismatch.rs:1:9
|
||||
|
|
||||
LL | #![warn(argument_comment_mismatch)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
warning: 1 warning emitted
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#![warn(uncommented_anonymous_literal_argument)]
|
||||
|
||||
fn main() {
|
||||
let line = "{\"type\":\"response_item\"}";
|
||||
let _ = line.starts_with('{');
|
||||
let _ = line.find("type");
|
||||
let parts = ["type", "response_item"];
|
||||
let _ = parts.join("\n");
|
||||
}
|
||||
18
tools/argument-comment-lint/ui/uncommented_literal.rs
Normal file
18
tools/argument-comment-lint/ui/uncommented_literal.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
#![warn(uncommented_anonymous_literal_argument)]
|
||||
|
||||
struct Client;
|
||||
|
||||
impl Client {
|
||||
fn set_legacy_flag(&self, enabled: bool) {}
|
||||
}
|
||||
|
||||
fn legacy_create_openai_url(base_url: Option<String>, retry_count: usize) -> String {
|
||||
let _ = (base_url, retry_count);
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let client = Client;
|
||||
let _ = legacy_create_openai_url(None, 3);
|
||||
client.set_legacy_flag(true);
|
||||
}
|
||||
26
tools/argument-comment-lint/ui/uncommented_literal.stderr
Normal file
26
tools/argument-comment-lint/ui/uncommented_literal.stderr
Normal file
@@ -0,0 +1,26 @@
|
||||
warning: anonymous literal-like argument for parameter `base_url`
|
||||
--> $DIR/uncommented_literal.rs:16:38
|
||||
|
|
||||
LL | let _ = legacy_create_openai_url(None, 3);
|
||||
| ^^^^ help: prepend the parameter name comment: `/*base_url=*/ None`
|
||||
|
|
||||
note: the lint level is defined here
|
||||
--> $DIR/uncommented_literal.rs:1:9
|
||||
|
|
||||
LL | #![warn(uncommented_anonymous_literal_argument)]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
warning: anonymous literal-like argument for parameter `retry_count`
|
||||
--> $DIR/uncommented_literal.rs:16:44
|
||||
|
|
||||
LL | let _ = legacy_create_openai_url(None, 3);
|
||||
| ^ help: prepend the parameter name comment: `/*retry_count=*/ 3`
|
||||
|
||||
warning: anonymous literal-like argument for parameter `enabled`
|
||||
--> $DIR/uncommented_literal.rs:17:28
|
||||
|
|
||||
LL | client.set_legacy_flag(true);
|
||||
| ^^^^ help: prepend the parameter name comment: `/*enabled=*/ true`
|
||||
|
||||
warning: 3 warnings emitted
|
||||
|
||||
Reference in New Issue
Block a user