code-moniker-check 0.7.1

Rules engine for code-moniker: DSL, rule config/profiles, evaluation over the symbol graph, and suppression.
Documentation
use std::fs;
use std::path::Path;

use code_moniker_check::workspace::{
	WorkspaceCheckRunner, WorkspaceCheckRunnerOptions, WorkspaceEvaluationMode,
};
use code_moniker_check::{CheckRequest, DefaultRulesSelection, RuleSetRequest};
use code_moniker_workspace::registry::{LocalWorkspaceOptions, LocalWorkspaceRegistry};
use code_moniker_workspace::snapshot::{WorkspaceRequest, WorkspaceTransition};
use code_moniker_workspace::source::LocalResourceCache;

const SCHEME: &str = "code+moniker://";
const RULE_ID: &str = "workspace.group.unique-type-name-per-package";
const STATISTIC_RULE_ID: &str = "workspace.group.balanced-invoice-size";

fn write(root: &Path, path: &str, source: &str) {
	let target = root.join(path);
	fs::create_dir_all(target.parent().expect("fixture parent")).expect("fixture directory");
	fs::write(target, source).expect("fixture source");
}

fn group_fixture() -> (tempfile::TempDir, std::path::PathBuf) {
	let fixture = tempfile::tempdir().expect("workspace fixture");
	for (path, package, container, member) in [
		(
			"src/main/java/com/acme/sales/SalesA.java",
			"com.acme.sales",
			"SalesA",
			"Invoice",
		),
		(
			"src/main/java/com/acme/sales/SalesB.java",
			"com.acme.sales",
			"SalesB",
			"Invoice",
		),
		(
			"src/main/java/com/acme/legacy/LegacyA.java",
			"com.acme.legacy",
			"LegacyA",
			"Legacy",
		),
		(
			"src/main/java/com/acme/legacy/LegacyB.java",
			"com.acme.legacy",
			"LegacyB",
			"Legacy",
		),
	] {
		write(
			fixture.path(),
			path,
			&format!("package {package};\n\nclass {container} {{\n\tclass {member} {{}}\n}}\n"),
		);
	}
	let rules = fixture.path().join(".code-moniker.toml");
	fs::write(
		&rules,
		r#"
default_rules = false

[[workspace.group.where]]
id = "unique-type-name-per-package"
severity = "warn"
members = "shape = 'type'"
group_by = ["lang", "segment('package')", "name"]
expr = "count(member) <= 1"
message = "Duplicate type group {group}: {members}"
suppress = [{ values = ["java", "com.acme.legacy", "Legacy"] }]
"#,
	)
	.expect("rules");
	(fixture, rules)
}

fn assert_skipped_generation_falls_back(
	root: &Path,
	rules: &Path,
	registry: &mut LocalWorkspaceRegistry,
	runner: &mut WorkspaceCheckRunner,
	cache: &LocalResourceCache,
) {
	let source_path = root.join("src/main/java/com/acme/sales/SalesA.java");
	for method in ["first", "second"] {
		write(
			root,
			"src/main/java/com/acme/sales/SalesA.java",
			&format!(
				"package com.acme.sales;\n\nclass SalesA {{\n\tclass Invoice {{}}\n\tvoid {method}() {{}}\n}}\n"
			),
		);
		let transition = registry.commands().refresh_paths(
			WorkspaceRequest::new("workspace-group-skipped-generation"),
			vec![source_path.clone()],
		);
		assert!(matches!(transition, WorkspaceTransition::Ready { .. }));
	}
	let skipped = registry
		.queries()
		.snapshot()
		.expect("snapshot after skipped generation");
	let fallback = runner
		.run_check(&skipped.index, &skipped.linkage)
		.expect("safe full fallback");
	let mut cold = WorkspaceCheckRunner::new(
		WorkspaceCheckRunnerOptions::new(rules.to_path_buf(), None, SCHEME),
		cache.clone(),
	);
	let expected = cold
		.run_check(&skipped.index, &skipped.linkage)
		.expect("cold result after skipped generation");
	assert_eq!(fallback.diagnostics, expected.diagnostics);
	assert_eq!(fallback.evaluation.mode, WorkspaceEvaluationMode::Full);
}

#[test]
fn group_collision_is_single_stable_diagnostic_across_one_shot_and_snapshot() {
	let (fixture, rules) = group_fixture();
	let specs = RuleSetRequest::with_rules(&rules, SCHEME)
		.with_default_rules(DefaultRulesSelection::Disabled)
		.compiled_specs_for_langs(std::iter::empty())
		.expect("workspace group specs");
	assert_eq!(specs.len(), 1);
	assert_eq!(specs[0].root, "workspace");
	assert_eq!(specs[0].subject, "group");
	assert_eq!(specs[0].plan, "t1_inventory");

	let one_shot = CheckRequest::new(
		fixture.path(),
		RuleSetRequest::with_rules(&rules, SCHEME)
			.with_default_rules(DefaultRulesSelection::Disabled),
	)
	.run()
	.expect("one-shot group check");
	let violations = one_shot.file_violations().collect::<Vec<_>>();
	assert_eq!(violations.len(), 1, "{violations:#?}");
	assert_eq!(violations[0].1.rule_id, RULE_ID);
	assert!(violations[0].0.ends_with("SalesA.java"));
	assert!(violations[0].1.message.contains("com.acme.sales"));
	assert!(violations[0].1.message.contains("2 members"));

	let cache = LocalResourceCache::default();
	let mut registry = LocalWorkspaceRegistry::local_with_cache(
		LocalWorkspaceOptions::new(vec![fixture.path().to_path_buf()], None),
		cache.clone(),
	);
	let transition = registry
		.commands()
		.refresh(WorkspaceRequest::new("workspace-group-runner"));
	assert!(matches!(transition, WorkspaceTransition::Ready { .. }));
	let snapshot = registry.queries().snapshot().expect("snapshot");
	let mut runner = WorkspaceCheckRunner::new(
		WorkspaceCheckRunnerOptions::new(rules.clone(), None, SCHEME),
		cache.clone(),
	);
	let diagnostics = runner
		.run_check(&snapshot.index, &snapshot.linkage)
		.expect("snapshot group check");
	let group_diagnostics = diagnostics
		.diagnostics
		.iter()
		.filter(|diagnostic| diagnostic.rule_id == RULE_ID)
		.collect::<Vec<_>>();
	assert_eq!(group_diagnostics.len(), 1, "{group_diagnostics:#?}");
	let primary = group_diagnostics[0]
		.symbol
		.expect("group diagnostic primary symbol");
	assert_eq!(
		snapshot
			.index
			.symbols
			.file_records(primary.file())
			.get(primary.def())
			.expect("primary group member")
			.name,
		"Invoice"
	);

	write(
		fixture.path(),
		"src/main/java/com/acme/sales/SalesB.java",
		"package com.acme.orders;\n\nclass SalesB {\n\tclass Invoice {}\n}\n",
	);
	let transition = registry.commands().refresh_paths(
		WorkspaceRequest::new("workspace-group-move"),
		vec![
			fixture
				.path()
				.join("src/main/java/com/acme/sales/SalesB.java"),
		],
	);
	assert!(matches!(transition, WorkspaceTransition::Ready { .. }));
	let moved = registry.queries().snapshot().expect("moved snapshot");
	let incremental = runner
		.run_check(&moved.index, &moved.linkage)
		.expect("incremental group check");
	let mut cold_runner = WorkspaceCheckRunner::new(
		WorkspaceCheckRunnerOptions::new(rules.clone(), None, SCHEME),
		cache.clone(),
	);
	let cold = cold_runner
		.run_check(&moved.index, &moved.linkage)
		.expect("cold group check");
	assert_eq!(incremental.diagnostics, cold.diagnostics);
	assert_eq!(
		incremental.evaluation.mode,
		WorkspaceEvaluationMode::Incremental
	);
	assert_eq!(incremental.evaluation.dirty_symbols, 6);
	assert_eq!(incremental.evaluation.evaluated_symbols, 3);
	assert_eq!(incremental.evaluation.affected_groups, 4);
	assert!(incremental.evaluation.evaluated_symbols < moved.index.inventory.all_symbols().len());
	assert_skipped_generation_falls_back(
		fixture.path(),
		&rules,
		&mut registry,
		&mut runner,
		&cache,
	);
}

#[test]
fn changing_group_suppression_invalidates_cached_evaluation() {
	let (fixture, rules) = group_fixture();
	let cache = LocalResourceCache::default();
	let mut registry = LocalWorkspaceRegistry::local_with_cache(
		LocalWorkspaceOptions::new(vec![fixture.path().to_path_buf()], None),
		cache.clone(),
	);
	let transition = registry
		.commands()
		.refresh(WorkspaceRequest::new("workspace-group-suppression-seed"));
	assert!(matches!(transition, WorkspaceTransition::Ready { .. }));
	let snapshot = registry.queries().snapshot().expect("snapshot");
	let mut runner = WorkspaceCheckRunner::new(
		WorkspaceCheckRunnerOptions::new(rules.clone(), None, SCHEME),
		cache.clone(),
	);
	let initial = runner
		.run_check(&snapshot.index, &snapshot.linkage)
		.expect("initial group evaluation");
	assert_eq!(
		initial
			.diagnostics
			.iter()
			.filter(|diagnostic| diagnostic.rule_id == RULE_ID)
			.count(),
		1
	);

	let updated = fs::read_to_string(&rules)
		.expect("group rules")
		.replace(
			"suppress = [{ values = [\"java\", \"com.acme.legacy\", \"Legacy\"] }]",
			"suppress = [{ values = [\"java\", \"com.acme.legacy\", \"Legacy\"] }, { values = [\"java\", \"com.acme.sales\", \"Invoice\"] }]",
		);
	fs::write(&rules, updated).expect("updated group suppression");
	let cached = runner
		.run_check(&snapshot.index, &snapshot.linkage)
		.expect("evaluation after suppression change");
	let mut cold =
		WorkspaceCheckRunner::new(WorkspaceCheckRunnerOptions::new(rules, None, SCHEME), cache);
	let expected = cold
		.run_check(&snapshot.index, &snapshot.linkage)
		.expect("cold evaluation after suppression change");
	assert_eq!(cached.evaluation.mode, WorkspaceEvaluationMode::Full);
	assert_eq!(cached.diagnostics, expected.diagnostics);
}

#[test]
fn hot_index_recomputes_line_statistics_for_the_affected_group() {
	let fixture = tempfile::tempdir().expect("workspace fixture");
	for container in ["SalesA", "SalesB", "SalesC"] {
		write(
			fixture.path(),
			&format!("src/main/java/com/acme/sales/{container}.java"),
			&format!("package com.acme.sales;\n\nclass {container} {{\n\tclass Invoice {{}}\n}}\n"),
		);
	}
	let rules = fixture.path().join(".code-moniker.toml");
	fs::write(
		&rules,
		r#"
default_rules = false

[[workspace.group.where]]
id = "balanced-invoice-size"
severity = "warn"
members = "name = 'Invoice'"
group_by = ["lang", "segment('package')"]
expr = "count(member) >= 3 => avg(member, lines) <= 2"
message = "Invoice size distribution in {group}: {observations}"
"#,
	)
	.expect("statistic rules");
	let cache = LocalResourceCache::default();
	let mut registry = LocalWorkspaceRegistry::local_with_cache(
		LocalWorkspaceOptions::new(vec![fixture.path().to_path_buf()], None),
		cache.clone(),
	);
	let transition = registry
		.commands()
		.refresh(WorkspaceRequest::new("workspace-group-statistic-seed"));
	assert!(matches!(transition, WorkspaceTransition::Ready { .. }));
	let initial = registry.queries().snapshot().expect("initial snapshot");
	let mut runner = WorkspaceCheckRunner::new(
		WorkspaceCheckRunnerOptions::new(rules.clone(), None, SCHEME),
		cache.clone(),
	);
	let seeded = runner
		.run_check(&initial.index, &initial.linkage)
		.expect("seeded statistic evaluation");
	assert!(
		seeded
			.diagnostics
			.iter()
			.all(|diagnostic| diagnostic.rule_id != STATISTIC_RULE_ID)
	);

	let changed_path = fixture
		.path()
		.join("src/main/java/com/acme/sales/SalesC.java");
	write(
		fixture.path(),
		"src/main/java/com/acme/sales/SalesC.java",
		"package com.acme.sales;\n\nclass SalesC {\n\tclass Invoice {\n\t\tint first;\n\t\tint second;\n\t\tint third;\n\t\tint fourth;\n\t}\n}\n",
	);
	let transition = registry.commands().refresh_paths(
		WorkspaceRequest::new("workspace-group-statistic-change"),
		vec![changed_path],
	);
	assert!(matches!(transition, WorkspaceTransition::Ready { .. }));
	let changed = registry.queries().snapshot().expect("changed snapshot");
	let incremental = runner
		.run_check(&changed.index, &changed.linkage)
		.expect("incremental statistic evaluation");
	let mut cold_runner =
		WorkspaceCheckRunner::new(WorkspaceCheckRunnerOptions::new(rules, None, SCHEME), cache);
	let cold = cold_runner
		.run_check(&changed.index, &changed.linkage)
		.expect("cold statistic evaluation");

	assert_eq!(
		incremental.evaluation.mode,
		WorkspaceEvaluationMode::Incremental
	);
	assert_eq!(incremental.evaluation.affected_groups, 1);
	assert_eq!(incremental.diagnostics, cold.diagnostics);
	let statistic = incremental
		.diagnostics
		.iter()
		.find(|diagnostic| diagnostic.rule_id == STATISTIC_RULE_ID)
		.expect("statistic violation");
	assert!(statistic.message.contains("avg(member, lines)="));
	assert!(statistic.message.contains("3/3 line ranges"));
}