Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
75160b3
feat: add no-mistakes-config rule for path, glob, and limit lints
jonathanong Aug 22, 2026
685a45a
style: rustfmt filesystem_dispatch imports
jonathanong Aug 22, 2026
5d45c15
refactor: split no-mistakes-config path collection under 200 lines
jonathanong Aug 22, 2026
0a7b549
fix: export frameworks for sibling no-mistakes-config lints
jonathanong Aug 22, 2026
78f569d
fix: avoid clippy cloned_ref_to_slice_refs in glob lint
jonathanong Aug 22, 2026
6427f8d
fix: run no-mistakes-config from check and honor implicit Direct
jonathanong Aug 22, 2026
3036d04
style: rustfmt env-limit helper
jonathanong Aug 22, 2026
6229b90
fix: cover config-lint branches and treat . as the repo root
jonathanong Aug 22, 2026
05c6e3d
fix: skip env-limit Direct warning when all tests are selected
jonathanong Aug 22, 2026
686afe9
test: cover invalid glob errors and missing file/path kind labels
jonathanong Aug 22, 2026
cd08743
merge: origin/main into no-mistakes-config lint
jonathanong Aug 23, 2026
5cfdcc4
merge: origin/main after #764
jonathanong Aug 23, 2026
3078e59
feat: add workflow-topology-policy rule and step-order helpers (#763)
jonathanong Aug 23, 2026
1bec3ad
merge: origin/main after #765 and #767
jonathanong Aug 23, 2026
a184b0b
merge: origin/main after #759, #760, and #762
jonathanong Aug 23, 2026
3803086
merge: origin/main after #768
jonathanong Aug 23, 2026
c790307
fix: restore query-annotation and production-deps rule IDs after merge
jonathanong Aug 23, 2026
0a057a4
test: cover topology-policy graph, inventory, and step-order branches
jonathanong Aug 23, 2026
71a07c9
merge: origin/main after #769
jonathanong Aug 23, 2026
fb437ff
merge origin/main into feat/no-mistakes-config-lint
jonathanong Aug 23, 2026
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
2 changes: 2 additions & 0 deletions crates/no-mistakes/src/check_tasks/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const FILESYSTEM_RULE_IDS: &[&str] = &[
rules::NEXTJS_REDIRECT_DESTINATIONS,
rules::NO_EMPTY_OR_COMMENTS_ONLY_FILES,
rules::NO_GIT_IDENTITY_MUTATION,
rules::NO_MISTAKES_CONFIG,
rules::NO_RAW_EPHEMERAL_PORT,
rules::PACKAGE_JSON_REGISTRY_ONLY,
rules::PACKAGE_JSON_WORKSPACE_COVERAGE,
Expand Down Expand Up @@ -63,6 +64,7 @@ const FILESYSTEM_RULE_IDS: &[&str] = &[
rules::VITEST_CI_PATH_COVERAGE,
rules::VITEST_PROJECT_MAPPING,
rules::VITEST_TEST_CORRESPONDENCE,
rules::WORKFLOW_TOPOLOGY_POLICY,
rules::WORKSPACE_PACKAGE_CYCLES,
];

Expand Down
34 changes: 17 additions & 17 deletions crates/no-mistakes/src/codebase/rules/filesystem_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ use super::{
markdown_child_links, markdown_eval_tests, markdown_link_display_text,
markdown_mermaid_validation, markdown_reachability, markdown_structure_budget,
nextjs_redirect_destinations, no_empty_or_comments_only_files, no_git_identity_mutation,
no_raw_ephemeral_port, package_json_registry_only, package_json_workspace_coverage,
postgres_constraint_validate, postgres_fk_index, postgres_lock_ordering,
postgres_no_add_column, postgres_no_generated_column_writes, postgres_no_offset,
postgres_redundant_index, postgres_require_named_constraints,
no_mistakes_config, no_raw_ephemeral_port, package_json_registry_only,
package_json_workspace_coverage, postgres_constraint_validate, postgres_fk_index,
postgres_lock_ordering, postgres_no_add_column, postgres_no_generated_column_writes,
postgres_no_offset, postgres_redundant_index, postgres_require_named_constraints,
postgres_require_query_annotation, production_dependency_declarations,
require_files_in_subdirs, require_test_per_subdir, required_companion_imports,
required_local_docs, rust_rules_combined, shellcheck_runner, strict_package_layout,
structured_config_policy, test_email_domain_policy, test_no_dependency_pins,
tsconfig_alias_folder_mapping, tsconfig_file_coverage, tsconfig_gate_coverage,
version_pin_consistency, vitest_ci_path_coverage, vitest_project_mapping,
vitest_test_correspondence, workspace_package_cycles,
vitest_test_correspondence, workflow_topology_policy, workspace_package_cycles,
};

mod candidate_helpers;
Expand All @@ -42,18 +42,18 @@ use super::{
INTEGRATION_TEST_NO_MOCKS, LOCKFILE_ALLOWLIST, MARKDOWN_CHILD_LINKS, MARKDOWN_EVAL_TESTS,
MARKDOWN_LINK_DISPLAY_TEXT, MARKDOWN_MERMAID_VALIDATION, MARKDOWN_REACHABILITY,
MARKDOWN_STRUCTURE_BUDGET, NEXTJS_REDIRECT_DESTINATIONS, NO_EMPTY_OR_COMMENTS_ONLY_FILES,
NO_GIT_IDENTITY_MUTATION, NO_RAW_EPHEMERAL_PORT, PACKAGE_JSON_REGISTRY_ONLY,
PACKAGE_JSON_WORKSPACE_COVERAGE, POSTGRES_CONSTRAINT_VALIDATE, POSTGRES_FK_INDEX,
POSTGRES_LOCK_ORDERING, POSTGRES_NO_ADD_COLUMN, POSTGRES_NO_GENERATED_COLUMN_WRITES,
POSTGRES_NO_OFFSET, POSTGRES_REDUNDANT_INDEX, POSTGRES_REQUIRE_NAMED_CONSTRAINTS,
POSTGRES_REQUIRE_QUERY_ANNOTATION, PRODUCTION_DEPENDENCY_DECLARATIONS,
REQUIRED_COMPANION_IMPORTS, REQUIRED_DOC_SECTION, REQUIRED_LOCAL_DOCS,
REQUIRE_FILES_IN_SUBDIRS, REQUIRE_TEST_PER_SUBDIR, RUST_MAX_LINES_PER_FILE,
RUST_NO_INLINE_ALLOWS, RUST_NO_INLINE_TESTS, SHELLCHECK_RUNNER, STRICT_PACKAGE_LAYOUT,
STRUCTURED_CONFIG_POLICY, TEST_EMAIL_DOMAIN_POLICY, TEST_NO_DEPENDENCY_PINS,
TSCONFIG_ALIAS_FOLDER_MAPPING, TSCONFIG_FILE_COVERAGE, TSCONFIG_GATE_COVERAGE,
VITEST_CI_PATH_COVERAGE, VITEST_PROJECT_MAPPING, VITEST_TEST_CORRESPONDENCE,
WORKSPACE_PACKAGE_CYCLES,
NO_GIT_IDENTITY_MUTATION, NO_MISTAKES_CONFIG, NO_RAW_EPHEMERAL_PORT,
PACKAGE_JSON_REGISTRY_ONLY, PACKAGE_JSON_WORKSPACE_COVERAGE, POSTGRES_CONSTRAINT_VALIDATE,
POSTGRES_FK_INDEX, POSTGRES_LOCK_ORDERING, POSTGRES_NO_ADD_COLUMN,
POSTGRES_NO_GENERATED_COLUMN_WRITES, POSTGRES_NO_OFFSET, POSTGRES_REDUNDANT_INDEX,
POSTGRES_REQUIRE_NAMED_CONSTRAINTS, POSTGRES_REQUIRE_QUERY_ANNOTATION,
PRODUCTION_DEPENDENCY_DECLARATIONS, REQUIRED_COMPANION_IMPORTS, REQUIRED_DOC_SECTION,
REQUIRED_LOCAL_DOCS, REQUIRE_FILES_IN_SUBDIRS, REQUIRE_TEST_PER_SUBDIR,
RUST_MAX_LINES_PER_FILE, RUST_NO_INLINE_ALLOWS, RUST_NO_INLINE_TESTS, SHELLCHECK_RUNNER,
STRICT_PACKAGE_LAYOUT, STRUCTURED_CONFIG_POLICY, TEST_EMAIL_DOMAIN_POLICY,
TEST_NO_DEPENDENCY_PINS, TSCONFIG_ALIAS_FOLDER_MAPPING, TSCONFIG_FILE_COVERAGE,
TSCONFIG_GATE_COVERAGE, VITEST_CI_PATH_COVERAGE, VITEST_PROJECT_MAPPING,
VITEST_TEST_CORRESPONDENCE, WORKFLOW_TOPOLOGY_POLICY, WORKSPACE_PACKAGE_CYCLES,
};
pub use entrypoints::{
run_filesystem_rules, run_filesystem_rules_with_config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ macro_rules! filesystem_rules {
TSCONFIG_ALIAS_FOLDER_MAPPING => tsconfig_alias_folder_mapping::check_with_files,
TSCONFIG_FILE_COVERAGE => tsconfig_file_coverage::check_with_files,
NO_GIT_IDENTITY_MUTATION => no_git_identity_mutation::check_with_files,
NO_MISTAKES_CONFIG => no_mistakes_config::check_with_files,
Comment thread
jonathanong marked this conversation as resolved.
Comment thread
jonathanong marked this conversation as resolved.
NO_RAW_EPHEMERAL_PORT => no_raw_ephemeral_port::check_with_files,
MARKDOWN_EVAL_TESTS => markdown_eval_tests::check_with_files,
PACKAGE_JSON_REGISTRY_ONLY => package_json_registry_only::check_with_files,
Expand All @@ -32,6 +33,7 @@ macro_rules! filesystem_rules {
NO_EMPTY_OR_COMMENTS_ONLY_FILES => no_empty_or_comments_only_files::check_with_files,
NEXTJS_REDIRECT_DESTINATIONS => nextjs_redirect_destinations::check_with_files,
VITEST_TEST_CORRESPONDENCE => vitest_test_correspondence::check_with_files,
WORKFLOW_TOPOLOGY_POLICY => workflow_topology_policy::check_with_files,
FILE_EXTENSION_POLICY => file_extension_policy::check_with_files,
BANNED_PATHS => banned_paths::check_with_files,
BANNED_RENAMED_FILES => banned_renamed_files::check_with_files,
Expand Down
2 changes: 2 additions & 0 deletions crates/no-mistakes/src/codebase/rules/ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub use super::nextjs_no_caching::RULE_ID as NEXTJS_NO_CACHING;
pub use super::nextjs_redirect_destinations::RULE_ID as NEXTJS_REDIRECT_DESTINATIONS;
pub use super::no_empty_or_comments_only_files::RULE_ID as NO_EMPTY_OR_COMMENTS_ONLY_FILES;
pub use super::no_git_identity_mutation::RULE_ID as NO_GIT_IDENTITY_MUTATION;
pub use super::no_mistakes_config::RULE_ID as NO_MISTAKES_CONFIG;
pub use super::no_raw_ephemeral_port::RULE_ID as NO_RAW_EPHEMERAL_PORT;
pub use super::package_json_registry_only::RULE_ID as PACKAGE_JSON_REGISTRY_ONLY;
pub use super::package_json_workspace_coverage::RULE_ID as PACKAGE_JSON_WORKSPACE_COVERAGE;
Expand Down Expand Up @@ -62,4 +63,5 @@ pub use super::version_pin_consistency::RULE_ID as VERSION_PIN_CONSISTENCY;
pub use super::vitest_ci_path_coverage::RULE_ID as VITEST_CI_PATH_COVERAGE;
pub use super::vitest_project_mapping::RULE_ID as VITEST_PROJECT_MAPPING;
pub use super::vitest_test_correspondence::RULE_ID as VITEST_TEST_CORRESPONDENCE;
pub use super::workflow_topology_policy::RULE_ID as WORKFLOW_TOPOLOGY_POLICY;
pub use super::workspace_package_cycles::RULE_ID as WORKSPACE_PACKAGE_CYCLES;
2 changes: 2 additions & 0 deletions crates/no-mistakes/src/codebase/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub mod nextjs_no_caching;
pub mod nextjs_redirect_destinations;
pub mod no_empty_or_comments_only_files;
pub mod no_git_identity_mutation;
pub mod no_mistakes_config;
pub mod no_raw_ephemeral_port;
pub mod package_json_registry_only;
pub mod package_json_workspace_coverage;
Expand Down Expand Up @@ -68,6 +69,7 @@ pub mod vitest_ci_path_coverage;
mod vitest_project_catalog;
pub mod vitest_project_mapping;
pub mod vitest_test_correspondence;
pub mod workflow_topology_policy;
pub mod workspace_package_cycles;

pub mod filesystem_dispatch;
Expand Down
72 changes: 72 additions & 0 deletions crates/no-mistakes/src/codebase/rules/no_mistakes_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use super::RuleFinding;
use crate::codebase::ts_source::relative_slash_path;
use crate::config::v2::NoMistakesConfig;
use anyhow::Result;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

mod globs;
mod limits;
mod paths;

pub const RULE_ID: &str = "no-mistakes-config";

pub(crate) fn check_with_files(
root: &Path,
config: &NoMistakesConfig,
all_files: &[PathBuf],
) -> Result<Vec<RuleFinding>> {
let sources = super::source_store_for_files(all_files);
check_with_files_and_sources(root, config, all_files, &sources)
}

pub(crate) fn check_with_files_and_sources(
root: &Path,
config: &NoMistakesConfig,
all_files: &[PathBuf],
_sources: &crate::codebase::ts_source::SourceStore,
) -> Result<Vec<RuleFinding>> {
if !config.rule_configured(RULE_ID) {
return Ok(Vec::new());
}
let tracked = tracked_rels(root, all_files);
Comment thread
jonathanong marked this conversation as resolved.
let config_file = config_rel(root, all_files);
let mut findings = paths::lint(config, &tracked, &config_file)?;
findings.extend(globs::lint(config, &tracked, &config_file)?);
findings.extend(limits::lint(config, &config_file));
super::sort_findings(&mut findings);
Ok(findings)
}

fn tracked_rels(root: &Path, all_files: &[PathBuf]) -> BTreeSet<String> {
all_files
.iter()
.map(|path| relative_slash_path(root, path))
.filter(|rel| !rel.is_empty())
.collect()
}

fn config_rel(root: &Path, all_files: &[PathBuf]) -> String {
all_files
.iter()
.find_map(|path| {
let name = path.file_name()?.to_str()?;
(name == ".no-mistakes.yml" || name == ".no-mistakes.yaml")
.then(|| relative_slash_path(root, path))
})
.unwrap_or_else(|| ".no-mistakes.yml".to_string())
Comment thread
jonathanong marked this conversation as resolved.
}

pub(super) fn finding(config_file: &str, message: String) -> RuleFinding {
RuleFinding {
rule: RULE_ID.to_string(),
file: config_file.to_string(),
line: 1,
Comment thread
jonathanong marked this conversation as resolved.
message,
import: None,
target: None,
}
}

#[cfg(test)]
mod tests;
92 changes: 92 additions & 0 deletions crates/no-mistakes/src/codebase/rules/no_mistakes_config/globs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use super::finding;
use super::paths::frameworks;
use crate::codebase::rules::path_filter::GlobMatcher;
use crate::codebase::rules::RuleFinding;
use crate::config::v2::NoMistakesConfig;
use anyhow::Result;
use std::collections::BTreeSet;

pub(super) fn lint(
config: &NoMistakesConfig,
tracked: &BTreeSet<String>,
config_file: &str,
) -> Result<Vec<RuleFinding>> {
let mut findings = Vec::new();
for (framework, plan) in frameworks(config) {
for (env_name, env) in &plan.environments {
lint_patterns(
&format!("testPlan.{framework}.environments.{env_name}.include"),
&env.include,
tracked,
config_file,
&mut findings,
)?;
lint_patterns(
&format!("testPlan.{framework}.environments.{env_name}.exclude"),
&env.exclude,
tracked,
config_file,
&mut findings,
)?;
}
}
for (name, project) in &config.projects {
lint_patterns(
&format!("projects.{name}.include"),
&project.include,
tracked,
Comment thread
jonathanong marked this conversation as resolved.
config_file,
&mut findings,
)?;
lint_patterns(
&format!("projects.{name}.exclude"),
&project.exclude,
tracked,
config_file,
&mut findings,
)?;
}
for (index, rule) in config.rules.iter().enumerate() {
lint_patterns(
Comment thread
jonathanong marked this conversation as resolved.
&format!("rules[{index}].include"),
&rule.include,
Comment thread
jonathanong marked this conversation as resolved.
tracked,
config_file,
&mut findings,
)?;
lint_patterns(
&format!("rules[{index}].exclude"),
&rule.exclude,
tracked,
config_file,
&mut findings,
)?;
}
Ok(findings)
}

fn lint_patterns(
field: &str,
patterns: &[String],
tracked: &BTreeSet<String>,
config_file: &str,
findings: &mut Vec<RuleFinding>,
) -> Result<()> {
for (index, pattern) in patterns.iter().enumerate() {
if pattern.trim().is_empty() || !looks_like_glob(pattern) {
continue;
Comment thread
jonathanong marked this conversation as resolved.
}
let matcher = GlobMatcher::new(std::slice::from_ref(pattern), field)?;
if !tracked.iter().any(|rel| matcher.is_match(rel)) {
Comment thread
jonathanong marked this conversation as resolved.
findings.push(finding(
config_file,
format!("{field}[{index}]: glob `{pattern}` matches no tracked files"),
));
}
}
Ok(())
}

fn looks_like_glob(pattern: &str) -> bool {
pattern.contains('*') || pattern.contains('?') || pattern.contains('{')
}
43 changes: 43 additions & 0 deletions crates/no-mistakes/src/codebase/rules/no_mistakes_config/limits.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use super::finding;
use super::paths::frameworks;
use crate::codebase::rules::RuleFinding;
use crate::config::v2::schema::{TestPlanEnvironment, TestPlanGroupType, TestPlanLimit};
use crate::config::v2::NoMistakesConfig;

pub(super) fn lint(config: &NoMistakesConfig, config_file: &str) -> Vec<RuleFinding> {
let mut findings = Vec::new();
for (framework, plan) in frameworks(config) {
for (env_name, env) in &plan.environments {
if has_effective_limit(&env.limit) && !env.all && has_direct_group(env) {
findings.push(finding(
config_file,
format!(
"testPlan.{framework}.environments.{env_name} has a limit while a direct group exists; \
scope the budget onto non-direct groups so changed tests are not dropped (regression #9440)"
),
));
}
}
}
findings
}

fn has_direct_group(env: &TestPlanEnvironment) -> bool {
env.groups.is_empty()
|| env
.groups
.iter()
.any(|group| group.type_ == TestPlanGroupType::Direct)
}

fn has_effective_limit(limit: &Option<TestPlanLimit>) -> bool {
let Some(limit) = limit else {
return false;
};
limit.files.is_some()
|| limit
.percent
.as_ref()
.and_then(|percent| percent.value())
.is_some()
Comment thread
jonathanong marked this conversation as resolved.
}
Loading
Loading