Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Notable changes to JevGate. Versions follow [Semantic Versioning](https://semver

## [Unreleased]

Three changes to maintainability findings, each from the findings JevGate's own release check got wrong and measured on the corpus with every changed review and consider labeled by hand (a debatable one counting as not right): considers went from 57% to 59% right on the projects used for tuning, from 47% to 51% on the held-out ones and from 24% to 28% on 23 Bend 2 projects never used for tuning; no review changed but one wrong file-organization review. The self-check's baseline is empty, down from four accepted findings.

- Function simplification: splitting a function of 20 lines or fewer is at most a note. 12 of 39 such considers were right on the tuned projects and 5 of 50 on the blind Bend 2 ones: helpers that read in one look, a dispatch over a token's cases, proofs. Function-simplification considers went from 68% to 73% right. Nothing is asked again.
- Shared logic: copies of up to twelve lines between test cases in different files are notes, like short copies inside test cases: tests of separate modules or rules repeat the same setup because the code they test is parallel, and a helper shared across test files would couple them. 30 of 87 such considers were right on the tuned projects and 2 of 24 on the held-out ones; shared-logic considers went from 53% to 57% right there and from 43% to 53% on the held-out projects. Nothing is asked again.
- File organization: a split the recheck raised from an undecided first answer is asked what kind of file it is, as an undecided one is, and a kind that serves one feature clears it. The first pass and the recheck disagreeing was a weak sign: 4 of the 18 findings it cleared were right. About $0.01 on the corpus.

## [0.22.0] - 2026-09-27

Bend 2 ([bendlang/bend](https://github.com/bendlang/bend) 2.0.x) is a supported language, with a rule for its laws. Every review and consider on Bend code was labeled by hand from the code, a debatable one counting as not right. On the Bend repository and 40 community projects used for tuning, 58% of reviews and 43% of considers were right (62% and 44% for the default rules, without the opt-in security and documentation groups). On 23 community projects never used for tuning, 53% of reviews were right before one change made from their labels (a split of a Bend 2 file is at most a consider) and 70% with it (74% for the default rules), and about 27% of considers, from a sample of 150 of 427; hardcoded values were the weakest rule there (26% right), and law findings were right 15 times in 23. Every request to the other languages' code is unchanged on the 117 corpus projects.
Expand Down
39 changes: 1 addition & 38 deletions jevgate-baseline.json
Original file line number Diff line number Diff line change
@@ -1,42 +1,5 @@
{
"version": 1,
"created_at": 1790514454,
"findings": [
{
"fingerprint": "9a142118d2e3410e9f6c85cfb48ffb6f9ca83a6a7667b7a97bf02223aeddc1ca",
"rule": "maintainability/function-simplification",
"path": "src/analysis/test_map.rs",
"line": 208,
"strength": "consider",
"message": "`visit` likely mixes separate jobs; splitting it may make it easier to understand (0.85).",
"reason": "wrong"
},
{
"fingerprint": "4e8f1ec95e2bfba8dc17ef300ba3a88fae0a1322781dec1759520aca633a5844",
"rule": "maintainability/function-simplification",
"path": "src/units/outcome/mod.rs",
"line": 346,
"strength": "consider",
"message": "`law_recheck` likely mixes separate jobs; splitting it may make it easier to understand (0.82).",
"reason": "wrong"
},
{
"fingerprint": "30bba567a24271430ef643e3e8e4c7c87d4ecc59a9fa81906f4666e7f580b8b0",
"rule": "maintainability/file-organization",
"path": "src/units/plan/file.rs",
"line": 294,
"strength": "consider",
"message": "Some members of this file could move to a separate module (0.81). G2 (`laravel_config`, `plan_module`) would be most useful as its own module.",
"reason": "wrong"
},
{
"fingerprint": "a100d2c1b75c85bfed70d361281c8ba3dde35f409ede013f7ae6fec3286a20f5",
"rule": "maintainability/shared-logic",
"path": "src/units/tests/comments.rs",
"line": 40,
"strength": "consider",
"message": "`each_comment_is_asked_about_with_the_code_it_is_about` (src/units/tests/comments.rs:40) and `claims_that_quantify_under_a_comment_are_asked_with_their_reading_and_defs` (src/units/tests/laws.rs:34) repeat the same steps across test cases (0.86). Differences: `comments_project`\u2192`laws_project`, `comments`\u2192`laws`.",
"reason": "wrong"
}
]
"findings": []
}
180 changes: 100 additions & 80 deletions src/analysis/test_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,118 +212,138 @@ fn visit(
in_test_class: bool,
found: &mut Vec<TestCase>,
) {
if let Some(test_class) = class_context(node, source, pytest) {
visit_children(node, source, pytest, test_class, found);
return;
}
match case(node, source, in_test_class) {
Visit::Case {
node,
start,
name,
ruby,
} => {
push(node, start, name, source, found);
if ruby {
ruby_context(node, source, found);
}
}
Visit::Leaf => {}
Visit::Inside => visit_children(node, source, pytest, in_test_class, found),
}
}

/// Whether a class's methods are tests, for a class that decides it: a
/// JUnit 3 `TestCase` subclass, a PHPUnit test class, a Ruby class such as
/// `class OrderTest < Minitest::Test`, and any Python class.
fn class_context(node: Node<'_>, source: &str, pytest: bool) -> Option<bool> {
match node.kind() {
"class_declaration"
if crate::test_locations::junit3_class(node, source)
|| crate::analysis::php::test_class(node, source) =>
{
Some(true)
}
"class_definition" => Some(crate::test_locations::python_test_class(
node, source, pytest,
)),
"class" if crate::test_locations::ruby_test_class(node, source) => Some(true),
_ => None,
}
}

/// What a node is to the walk for test cases.
enum Visit<'a> {
/// A test case: the node, where it starts (a Rust test at its first
/// attribute), its name, and whether it is a Ruby example, which reads
/// the setup of its groups.
Case {
node: Node<'a>,
start: usize,
name: String,
ruby: bool,
},
/// A function or method that is not a test: nothing inside it is one.
Leaf,
/// Anything else: its children may hold test cases.
Inside,
}

fn case<'a>(node: Node<'a>, source: &str, in_test_class: bool) -> Visit<'a> {
let found = |node: Node<'a>, start, name| Visit::Case {
node,
start,
name,
ruby: false,
};
match node.kind() {
"function_item" => {
let marked = crate::test_locations::preceding_attributes(node, source)
.iter()
.any(|attribute| crate::test_locations::attribute_marks_test(attribute));
if marked {
push(
node,
attribute_start(node),
name(node, source),
source,
found,
);
found(node, attribute_start(node), name(node, source))
} else {
Visit::Leaf
}
return;
}
"function_declaration" if crate::test_locations::go_test_function(node, source) => {
push(node, node.start_byte(), name(node, source), source, found);
return;
}
// PHP: a test method of a PHPUnit test class.
"method_declaration"
if in_test_class && crate::analysis::php::test_method(node, source) =>
{
push(node, node.start_byte(), name(node, source), source, found);
return;
found(node, node.start_byte(), name(node, source))
}
// PHP: a test method of a PHPUnit test class; C# and Java test
// methods; `test…` methods of a JUnit 3 class.
"method_declaration" => {
if crate::test_locations::csharp_test_method(node, source)
let php = in_test_class && crate::analysis::php::test_method(node, source);
if php
|| crate::test_locations::csharp_test_method(node, source)
|| crate::test_locations::java_test_method(node, source)
|| in_test_class && name(node, source).starts_with("test")
{
push(node, node.start_byte(), name(node, source), source, found);
found(node, node.start_byte(), name(node, source))
} else {
Visit::Leaf
}
return;
}
// A JUnit 3 `TestCase` subclass: its `test…` methods are tests.
"class_declaration" if crate::test_locations::junit3_class(node, source) => {
visit_children(node, source, pytest, true, found);
return;
}
"function_definition" => {
let test = name(node, source).starts_with("test");
let outer = node
.parent()
.filter(|p| p.kind() == "decorated_definition")
.unwrap_or(node);
let top_level = outer.parent().is_some_and(|p| p.kind() == "module");
if test && (top_level || in_test_class) {
push(outer, outer.start_byte(), name(node, source), source, found);
}
return;
}
"class_definition" => {
let test_class = crate::test_locations::python_test_class(node, source, pytest);
visit_children(node, source, pytest, test_class, found);
return;
}
// Ruby: `class OrderTest < Minitest::Test` holds `test_*` methods.
"class" if crate::test_locations::ruby_test_class(node, source) => {
visit_children(node, source, pytest, true, found);
return;
}
"method" => {
let name = name(node, source);
if in_test_class && name.starts_with("test_") {
push(node, node.start_byte(), name, source, found);
ruby_context(node, source, found);
if name(node, source).starts_with("test") && (top_level || in_test_class) {
found(outer, outer.start_byte(), name(node, source))
} else {
Visit::Leaf
}
return;
}
"method" if in_test_class && name(node, source).starts_with("test_") => Visit::Case {
node,
start: node.start_byte(),
name: name(node, source),
ruby: true,
},
"method" => Visit::Leaf,
"call"
if crate::test_locations::ruby_test_call(node, source)
&& ruby::CASES.contains(&ruby::method(node, source)) =>
{
push(
Visit::Case {
node,
node.start_byte(),
ruby_case_name(node, source),
source,
found,
);
ruby_context(node, source, found);
return;
}
"class_declaration" if crate::analysis::php::test_class(node, source) => {
visit_children(node, source, pytest, true, found);
return;
}
"expression_statement" => {
if let Some(case) = crate::analysis::php::pest_statement(node, source)
&& !case.suite
{
push(node, node.start_byte(), case.title, source, found);
return;
}
}
"call_expression" => {
if let Some(case) = javascript_case(node, source) {
push(
statement(node),
statement(node).start_byte(),
case,
source,
found,
);
return;
start: node.start_byte(),
name: ruby_case_name(node, source),
ruby: true,
}
}
_ => {}
"expression_statement" => match crate::analysis::php::pest_statement(node, source) {
Some(case) if !case.suite => found(node, node.start_byte(), case.title),
_ => Visit::Inside,
},
"call_expression" => match javascript_case(node, source) {
Some(title) => found(statement(node), statement(node).start_byte(), title),
None => Visit::Inside,
},
_ => Visit::Inside,
}
visit_children(node, source, pytest, in_test_class, found);
}

fn visit_children(
Expand Down
6 changes: 3 additions & 3 deletions src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ pub fn rules() -> Vec<Rule> {

pub fn rule_version(key: &str) -> &'static str {
match key {
FILE_ORGANIZATION => "20",
FUNCTION_SIMPLIFICATION => "15",
SHARED_LOGIC => "21",
FILE_ORGANIZATION => "21",
FUNCTION_SIMPLIFICATION => "16",
SHARED_LOGIC => "22",
TEST_VALUE => "7",
TEST_REDUNDANCY => "4",
INJECTION => "10",
Expand Down
10 changes: 5 additions & 5 deletions src/tests/gating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use super::*;
#[test]
fn gate_fails_only_on_the_configured_results() {
let project = Project::new();
project.write("lib.rs", &function("f"));
project.write("lib.rs", &long_function("f"));
let mut options = args();
// Mock level, report status, the default gate's exit code, and the gate that fails it.
let cases = [
Expand Down Expand Up @@ -38,7 +38,7 @@ fn gate_fails_only_on_the_configured_results() {
#[test]
fn a_rule_level_fails_the_gate_only_for_that_rule() {
let project = Project::new();
project.write("lib.rs", &function("f"));
project.write("lib.rs", &long_function("f"));
let mut options = args();
let mut mock = Mock {
level: 4,
Expand All @@ -63,8 +63,8 @@ fn a_rule_level_fails_the_gate_only_for_that_rule() {
#[test]
fn a_scope_makes_its_paths_report_only_while_other_files_gate() {
let project = Project::new();
project.write("src/lib.rs", &function("f"));
project.write("scripts/tool.rs", &function("g"));
project.write("src/lib.rs", &long_function("f"));
project.write("scripts/tool.rs", &long_function("g"));
let mut options = args();
options.fail_on = vec![options::FailOn::Consider];
let mut mock = Mock {
Expand Down Expand Up @@ -108,7 +108,7 @@ fn publish(project: &Project, report: &schema::Report) {
#[test]
fn baselined_findings_do_not_fail_the_gate_but_new_ones_do() {
let project = Project::new();
project.write("lib.rs", &function("f"));
project.write("lib.rs", &long_function("f"));
let options = args();
let mut review = Mock {
level: 2,
Expand Down
7 changes: 7 additions & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ pub(super) fn function(name: &str) -> String {
)
}

/// A function longer than twenty lines, whose split can be a consider.
pub(super) fn long_function(name: &str) -> String {
format!(
"fn {name}(values: &[i32]) -> i32 {{\n let mut total = 0;\n for value in values {{\n total += value;\n }}\n let mut largest = i32::MIN;\n for value in values {{\n if *value > largest {{\n largest = *value;\n }}\n }}\n let mut smallest = i32::MAX;\n for value in values {{\n if *value < smallest {{\n smallest = *value;\n }}\n }}\n let spread = largest - smallest;\n let doubled = total * 2;\n doubled + spread + 1\n}}\n"
)
}

/// Levels: 0 answers the bottom of every scale (clear), 1 the middle (consider,
/// or a note where the middle says the code is fine), 2 the top (review),
/// 3 spreads probability (uncertain), 4 leans to the top without reaching review.
Expand Down
5 changes: 3 additions & 2 deletions src/units/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ pub fn unkinded_units(plan: &FilePlan, judgments: &[Judgment]) -> BTreeSet<Strin
/// An outline or large document not yet asked its kind whose split Score
/// stayed undecided: the recheck's for an outline that has one, else the
/// first. A large document's split finding is asked its kind as well, since
/// its Score reads headings alone.
/// its Score reads headings alone, and so is an outline's finding its
/// recheck raised from an undecided first answer.
fn unkinded_split(unit: &UnitPlan, judgments: &[Judgment]) -> bool {
if ![catalog::FILE_ORGANIZATION, catalog::LARGE_DOCS].contains(&unit.rule)
|| !answers(judgments, &unit.id, Pass::Trace).is_empty()
Expand All @@ -385,7 +386,7 @@ fn unkinded_split(unit: &UnitPlan, judgments: &[Judgment]) -> bool {
.get("split")
.is_some_and(|a| match benefit(a) {
Outcome::Uncertain(_) => true,
Outcome::Consider(_) | Outcome::Review(_) => document,
Outcome::Consider(_) | Outcome::Review(_) => document || pass == Pass::Recheck,
_ => false,
})
}
Expand Down
15 changes: 14 additions & 1 deletion src/units/outcome/documentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,22 @@ pub(super) fn kind_share(answer: Option<&Answer>, kind: &str) -> Option<f64> {
(mass > 0.0).then(|| probabilities.get(kind).copied().unwrap_or(0.0) / mass)
}

/// A stale section's or finished plan's check, a cleanup at most.
pub(super) fn staleness_outcome<'a>(
get: &impl Fn(&str) -> Option<&'a Answer>,
detail: &Detail,
) -> Option<Outcome> {
let question = if matches!(detail, Detail::Plan { .. }) {
"plan"
} else {
"relies"
};
get(question).map(|a| stale_outcome(cleanup(noul(a)), get("role")))
}

/// An undecided staleness check, cleared when the section's missing names
/// are clearly not a current part of the repository.
pub(super) fn stale_outcome(outcome: Outcome, role: Option<&Answer>) -> Outcome {
fn stale_outcome(outcome: Outcome, role: Option<&Answer>) -> Outcome {
match outcome {
Outcome::Uncertain(_)
if kind_share(role, "repository").is_some_and(|share| at_least(1.0 - share)) =>
Expand Down
Loading
Loading