Skip to content

Commit b3985cb

Browse files
makinzmclaude
andauthored
feat: Naming convention check with DI refactoring (#66)
* [chore] tasks/20260322-naming-convention/ タスクファイルを作成 because of PR65 作業開始 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [test] naming convention チェック機能のテストを追加 (RED) because of NamingViolation 実装前にテストを先行コミット - RawName / NameKind エンティティ (src/domain/entity/name.rs) - LayerConfig に name_deny / name_targets フィールドを追加 - SeverityConfig に naming_violation フィールドを追加 - ViolationKind::NamingViolation を追加 - Parser トレイトに parse_names() メソッドを追加 - 各言語パーサーに parse_names() スタブ (todo!()) を追加 - ViolationDetector に detect_naming() スタブ (todo!()) を追加 - config / layer パーステスト, detect_naming() ユニットテストを追加 - E2E テスト tests/e2e_naming.rs と fixture tests/fixtures/naming/ を追加 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [fix] naming convention チェック機能を実装 (GREEN) because of 全テストをパスさせるため - LayerConfig に name_deny / name_targets フィールドを追加 (serde パース対応) - SeverityConfig に naming_violation フィールドを追加 (デフォルト "error") - ViolationKind::NamingViolation を追加 - Parser トレイトに parse_names() メソッドを追加 - 各言語パーサーに parse_names() 実装 (Rust/TypeScript/Python/Go/Java/Kotlin) - ViolationDetector::detect_naming() 実装 (大文字小文字区別なし・部分一致) - check_architecture::check() にファイルレベルチェックと detect_naming() 呼び出しを追加 - フォーマッター (terminal/json/github_actions) に NamingViolation の表示を追加 - 全既存テスト + 新規テスト (27件) すべてパス Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [refactor] docs/TODO.md と README.md に naming convention 機能を追記 because of PR 作成前チェックリストを満たすため - docs/TODO.md: PR65 完了チェック (naming_violation) を実装状況サマリーに追加 - README.md: feature matrix に Naming convention rules 行を追加 - README.md: [[layers]] Configuration Reference に name_deny / name_targets を追加 - README.md: Naming Convention Check のサンプル設定と説明を追加 - README.md: [severity] テーブルに naming_violation を追加 - tasks/20260322-naming-convention/TODO.md: 全サイクルを完了チェック - tasks/20260322-naming-convention/timeline.md: GREEN/REFACTOR フェーズのログを追記 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * add naming rule * [refactor] mille.toml から domain/usecase の name_deny を削除 because of 自己矛盾 mille 自体が多言語ツールのため、domain/usecase が go・python・java 等の 言語名を使うのは正当な設計。language 名を name_deny に列挙するのは 自己矛盾であり、誤検知を生んでいた。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [fix] name_allow 設定を追加: composite word の false positive を抑制 because of "category" 内の "go" 等を誤検知しないため name_deny のマッチング前に name_allow リストの文字列を除去する。 例: name_allow = ["category"] で "ImportCategory" が "go" に引っかからなくなる。 "GoCategory" のように standalone な keyword が残る場合は引き続き violation。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [test] name_deny_ignore フィールドとテストを追加 (RED) because of テストファイル等を naming check から除外する機能 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [fix] name_deny_ignore フィルタを実装 (GREEN) because of name_deny_ignore にマッチするファイルを naming check から除外するため Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [refactor] README.md と TODO.md に name_allow / name_deny_ignore を追記 because of PR 作成前チェックリストを満たすため Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [refactor] remove language-specific types from domain/usecase layers because of Clean Architecture naming convention compliance - Remove ResolveConfig, TsResolveConfig, GoResolveConfig, JavaResolveConfig, PythonResolveConfig from domain/entity/config.rs - Remove resolve field from MilleConfig - Add ResolveConfigGenerator trait in domain/repository (port) - Add DefaultResolveConfigGenerator in infrastructure (adapter) - Update TomlConfigRepository with two-pass parsing (load_with_resolve) - Update DispatchingResolver.from_resolve_config to accept toml::Value - Update runner.rs wiring to use new approach - Update generate_toml to accept &dyn ResolveConfigGenerator - Rename language-specific test names to pattern-based names - Replace language-specific comments with pattern descriptions - Result: 0 naming violations in domain/usecase, all 494 tests pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * [refactor] website にネーミング規則ドキュメントを追加 because of PR 作成前チェックリストを満たすため - naming.mdx (ja/en) 新規作成: name_deny / name_allow / name_targets / name_deny_ignore - severity.mdx に naming_violation を追記 - layers.mdx にネーミング関連フィールドを追記 - サイドバーに「ネーミング規則」を追加 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3828c60 commit b3985cb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3331
-365
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ One TOML config. Rust-powered. CI-ready. Supports multiple languages from a sing
2323
| Layer dependency rules (`dependency_mode`) ||||||||
2424
| External library rules (`external_mode`) ||||||||
2525
| DI method call rules (`allow_call_patterns`) ||||||||
26+
| Naming convention rules (`name_deny`) ||||||||
2627

2728
## Install
2829

@@ -432,6 +433,43 @@ Exit codes:
432433
| `external_mode` | `"opt-in"` or `"opt-out"` for external library usage |
433434
| `external_allow` | Allowed external packages (when `external_mode = "opt-in"`) |
434435
| `external_deny` | Forbidden external packages (when `external_mode = "opt-out"`) |
436+
| `name_deny` | Forbidden keywords for naming convention check (case-insensitive partial match) |
437+
| `name_allow` | Substrings to strip before `name_deny` check (e.g. `"category"` prevents `"go"` match inside it) |
438+
| `name_targets` | Targets to check: `"file"`, `"symbol"`, `"variable"`, `"comment"` (default: all) |
439+
| `name_deny_ignore` | Glob patterns for files to exclude from naming checks (e.g. `"**/test_*.rs"`) |
440+
441+
#### Naming Convention Check (`name_deny`)
442+
443+
Forbid infrastructure-specific keywords from appearing in a layer's names.
444+
445+
```toml
446+
[[layers]]
447+
name = "usecase"
448+
paths = ["src/usecase/**"]
449+
dependency_mode = "opt-out"
450+
deny = []
451+
external_mode = "opt-out"
452+
external_deny = []
453+
454+
# Usecase layer must not reference specific infrastructure technologies
455+
name_deny = ["gcp", "aws", "azure", "mysql", "postgres"]
456+
name_allow = ["category"] # "category" contains "go" but should not be flagged
457+
name_targets = ["file", "symbol", "variable", "comment"] # default: all targets
458+
name_deny_ignore = ["**/test_*.rs", "tests/**"] # exclude test files from naming checks
459+
```
460+
461+
**Rules:**
462+
- Case-insensitive (`GCP` = `gcp` = `Gcp`)
463+
- Partial match (`ManageGcp` also matches `gcp`)
464+
- `name_allow` strips listed substrings before matching (e.g. `"category"` prevents false positive on `"go"`)
465+
- `name_deny_ignore` excludes files matching glob patterns from naming checks entirely
466+
- `name_targets` restricts which entity types are checked:
467+
- `"file"`: file basename (e.g. `aws_client.rs`)
468+
- `"symbol"`: function, class, struct, enum, trait, interface, type alias names
469+
- `"variable"`: variable, const, let, static declaration names
470+
- `"comment"`: inline comment content
471+
- Supported languages: Rust, TypeScript, JavaScript, Python, Go, Java, Kotlin
472+
- Severity is controlled by `severity.naming_violation` (default: `"error"`)
435473

436474
### `[[layers.allow_call_patterns]]`
437475

@@ -472,13 +510,15 @@ Control the severity level of each violation type. Violations can be `"error"`,
472510
| `external_violation` | `"error"` | External library rule violated |
473511
| `call_pattern_violation` | `"error"` | DI entrypoint method call rule violated |
474512
| `unknown_import` | `"warning"` | Import that could not be classified |
513+
| `naming_violation` | `"error"` | Naming convention rule violated (`name_deny`) |
475514

476515
```toml
477516
[severity]
478517
dependency_violation = "warning" # treat as warning for gradual adoption
479518
external_violation = "error"
480519
call_pattern_violation = "error"
481520
unknown_import = "warning"
521+
naming_violation = "warning" # treat as warning while rolling out naming rules
482522
```
483523

484524
Use `--fail-on warning` to exit 1 even for warnings when integrating into CI gradually.

docs/TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- ✅ Kotlin 言語サポート — `.kt` ファイルのパース(tree-sitter-kotlin)・`[resolve.java]` リゾルバー共用、flat/Gradle レイアウト対応、E2E テスト追加
2929
-`mille init` Python namespace パッケージ修正 — `src/` レイアウトで `from src.domain...` を使うプロジェクトで `src``package_names` に自動追加されるよう修正(PR #62
3030
-`mille init` Python namespace インポートスキャン修正 — `from src.domain.entity import X` がレイヤー内部依存として正しく検出されるよう修正(`classify_py_import` フルパス返し + `resolve_to_known_dir` プレフィックス照合)、`src/main.py` 等の浅い階層ファイルがスキップされずレイヤーとして登録されるよう修正(PR #62
31+
- ✅ ネーミング規則チェック (`name_deny` / `name_targets` / `name_allow` / `name_deny_ignore`) — レイヤーごとに禁止キーワードを設定し、ファイル名・シンボル名・変数名・コメントに禁止キーワードが含まれる場合に `NamingViolation` を報告(大文字小文字区別なし・部分一致)。`name_allow` で false positive を抑制、`name_deny_ignore` でグロブパターンにマッチするファイルを除外可能。対応言語: Rust/TypeScript/Python/Go/Java/Kotlin。`severity.naming_violation` で重大度設定可(PR #65
3132

3233
以下は **設定ファイルにフィールドが存在しても、まだ動作していない** 項目です(README に掲載しないよう修正済み):
3334
(現在なし)

mille.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ allow = []
1111
deny = ["infrastructure", "usecase", "presentation"]
1212
external_mode = "opt-in"
1313
external_allow = ["serde"]
14+
name_deny = ["python", "go", "java", "kotlin", "javascript", "typescript", "ruby", "php", "csharp", "cpp", "rust"]
15+
name_allow = ["category", "algorithm", "diagonal", "cargo", "ergonomic"]
1416

1517
[[layers]]
1618
name = "infrastructure"
@@ -27,6 +29,8 @@ dependency_mode = "opt-in"
2729
allow = ["domain"]
2830
external_mode = "opt-in"
2931
external_allow = []
32+
name_deny = ["python", "go", "java", "kotlin", "javascript", "typescript", "ruby", "php", "csharp", "cpp", "rust"]
33+
name_allow = ["algorithm", "cargo", "ergonomic"]
3034

3135
[[layers]]
3236
name = "presentation"

src/domain/entity/config.rs

Lines changed: 147 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -17,57 +17,6 @@ pub struct IgnoreConfig {
1717
pub test_patterns: Vec<String>,
1818
}
1919

20-
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
21-
pub struct ResolveConfig {
22-
pub typescript: Option<TsResolveConfig>,
23-
pub go: Option<GoResolveConfig>,
24-
pub python: Option<PythonResolveConfig>,
25-
pub java: Option<JavaResolveConfig>,
26-
#[serde(default)]
27-
pub aliases: std::collections::HashMap<String, String>,
28-
}
29-
30-
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
31-
pub struct TsResolveConfig {
32-
pub tsconfig: String,
33-
}
34-
35-
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
36-
pub struct GoResolveConfig {
37-
pub module_name: String,
38-
}
39-
40-
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
41-
pub struct JavaResolveConfig {
42-
/// Base package name that identifies internal imports.
43-
/// e.g. "com.example.myapp" — imports starting with this prefix are Internal.
44-
/// If omitted, mille auto-detects from `pom_xml` or `build_gradle`.
45-
#[serde(default)]
46-
pub module_name: Option<String>,
47-
/// Path to pom.xml (relative to mille.toml). If set, `groupId.artifactId`
48-
/// is used as the module name when `module_name` is not explicitly specified.
49-
#[serde(default)]
50-
pub pom_xml: Option<String>,
51-
/// Path to build.gradle (relative to mille.toml). If set, `group.rootProject.name`
52-
/// is used as the module name when `module_name` is not explicitly specified.
53-
#[serde(default)]
54-
pub build_gradle: Option<String>,
55-
}
56-
57-
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
58-
pub struct PythonResolveConfig {
59-
/// Source root relative to project root. Optional — if omitted, mille derives it
60-
/// automatically from the importing file's path and `package_names`.
61-
/// NOTE: this field is currently not consumed by the resolver; the resolver always
62-
/// auto-derives the source root. The field is kept for forward compatibility.
63-
#[serde(default)]
64-
pub src_root: String,
65-
/// Top-level package names that are part of this project.
66-
/// Imports starting with any of these names are classified as Internal.
67-
#[serde(default)]
68-
pub package_names: Vec<String>,
69-
}
70-
7120
#[derive(Debug, Deserialize, PartialEq, Eq, Clone)]
7221
pub struct SeverityConfig {
7322
#[serde(default = "default_error")]
@@ -78,6 +27,8 @@ pub struct SeverityConfig {
7827
pub call_pattern_violation: String,
7928
#[serde(default = "default_warning")]
8029
pub unknown_import: String,
30+
#[serde(default = "default_error")]
31+
pub naming_violation: String,
8132
}
8233

8334
fn default_error() -> String {
@@ -94,7 +45,6 @@ pub struct MilleConfig {
9445
#[serde(rename = "layers", default)]
9546
pub layers: Vec<LayerConfig>,
9647
pub ignore: Option<IgnoreConfig>,
97-
pub resolve: Option<ResolveConfig>,
9848
#[serde(default = "default_severity")]
9949
pub severity: SeverityConfig,
10050
}
@@ -105,6 +55,7 @@ fn default_severity() -> SeverityConfig {
10555
external_violation: default_error(),
10656
call_pattern_violation: default_error(),
10757
unknown_import: default_warning(),
58+
naming_violation: default_error(),
10859
}
10960
}
11061

@@ -119,66 +70,185 @@ mod tests {
11970
use super::*;
12071

12172
#[test]
122-
fn test_python_resolve_config_without_src_root_parses() {
123-
// src_root なしの [resolve.python] は parse エラーにならないべき
73+
fn test_config_with_resolve_section_still_parses() {
74+
// [resolve] section is now handled by infrastructure's two-pass parsing,
75+
// but MilleConfig should still parse when [resolve] is absent.
12476
let toml = r#"
12577
[project]
12678
name = "myproject"
12779
root = "."
128-
languages = ["python"]
129-
130-
[resolve.python]
131-
package_names = ["domain", "usecase"]
80+
languages = ["rust"]
13281
13382
[[layers]]
13483
name = "domain"
13584
paths = ["src/domain/**"]
13685
dependency_mode = "opt-in"
13786
external_mode = "opt-in"
87+
"#;
88+
let result = toml::from_str::<MilleConfig>(toml);
89+
assert!(result.is_ok(), "parse should succeed: {:?}", result.err());
90+
}
91+
92+
#[test]
93+
fn test_layer_config_with_name_deny_parses() {
94+
// name_deny を含む [[layers]] が parse できる
95+
let toml = r#"
96+
[project]
97+
name = "myproject"
98+
root = "."
99+
languages = ["rust"]
100+
101+
[[layers]]
102+
name = "usecase"
103+
paths = ["src/usecase/**"]
104+
dependency_mode = "opt-out"
105+
external_mode = "opt-out"
106+
name_deny = ["aws", "gcp"]
107+
"#;
108+
let result = toml::from_str::<MilleConfig>(toml);
109+
assert!(
110+
result.is_ok(),
111+
"name_deny で parse できるべき: {:?}",
112+
result.err()
113+
);
114+
let config = result.unwrap();
115+
assert_eq!(config.layers[0].name_deny, vec!["aws", "gcp"]);
116+
}
117+
118+
#[test]
119+
fn test_layer_config_with_name_targets_parses() {
120+
use crate::domain::entity::layer::NameTarget;
121+
// name_targets を含む [[layers]] が parse できる
122+
let toml = r#"
123+
[project]
124+
name = "myproject"
125+
root = "."
126+
languages = ["rust"]
127+
128+
[[layers]]
129+
name = "usecase"
130+
paths = ["src/usecase/**"]
131+
dependency_mode = "opt-out"
132+
external_mode = "opt-out"
133+
name_deny = ["aws"]
134+
name_targets = ["file", "symbol"]
138135
"#;
139136
let result = toml::from_str::<MilleConfig>(toml);
140137
assert!(
141138
result.is_ok(),
142-
"src_root なしで parse できるべき: {:?}",
139+
"name_targets で parse できるべき: {:?}",
143140
result.err()
144141
);
145142
let config = result.unwrap();
146-
let py = config
147-
.resolve
148-
.unwrap()
149-
.python
150-
.expect("python config should exist");
151-
assert_eq!(py.src_root, "");
152-
assert_eq!(py.package_names, vec!["domain", "usecase"]);
143+
assert_eq!(
144+
config.layers[0].name_targets,
145+
vec![NameTarget::File, NameTarget::Symbol]
146+
);
153147
}
154148

155149
#[test]
156-
fn test_python_resolve_config_with_src_root_still_parses() {
157-
// 既存の src_root ありの設定は引き続き parse できる (regression)
150+
fn test_layer_config_name_targets_default_is_all() {
151+
use crate::domain::entity::layer::NameTarget;
152+
// name_targets を省略したとき全ターゲットがデフォルトになる
158153
let toml = r#"
159154
[project]
160155
name = "myproject"
161156
root = "."
162-
languages = ["python"]
157+
languages = ["rust"]
163158
164-
[resolve.python]
165-
src_root = "src"
166-
package_names = ["domain"]
159+
[[layers]]
160+
name = "usecase"
161+
paths = ["src/usecase/**"]
162+
dependency_mode = "opt-out"
163+
external_mode = "opt-out"
164+
name_deny = ["aws"]
165+
"#;
166+
let result = toml::from_str::<MilleConfig>(toml);
167+
assert!(
168+
result.is_ok(),
169+
"name_targets 省略で parse できるべき: {:?}",
170+
result.err()
171+
);
172+
let config = result.unwrap();
173+
assert_eq!(config.layers[0].name_targets, NameTarget::all());
174+
}
175+
176+
#[test]
177+
fn test_severity_config_with_naming_violation_parses() {
178+
// naming_violation = "error" を含む [severity] が parse できる
179+
let toml = r#"
180+
[project]
181+
name = "myproject"
182+
root = "."
183+
languages = ["rust"]
184+
185+
[[layers]]
186+
name = "usecase"
187+
paths = ["src/usecase/**"]
188+
dependency_mode = "opt-out"
189+
external_mode = "opt-out"
190+
191+
[severity]
192+
naming_violation = "error"
193+
"#;
194+
let result = toml::from_str::<MilleConfig>(toml);
195+
assert!(
196+
result.is_ok(),
197+
"naming_violation で parse できるべき: {:?}",
198+
result.err()
199+
);
200+
let config = result.unwrap();
201+
assert_eq!(config.severity.naming_violation, "error");
202+
}
203+
204+
#[test]
205+
fn test_severity_config_naming_violation_default_is_error() {
206+
// naming_violation を省略したときデフォルトは "error"
207+
let toml = r#"
208+
[project]
209+
name = "myproject"
210+
root = "."
211+
languages = ["rust"]
212+
213+
[[layers]]
214+
name = "usecase"
215+
paths = ["src/usecase/**"]
216+
dependency_mode = "opt-out"
217+
external_mode = "opt-out"
218+
"#;
219+
let result = toml::from_str::<MilleConfig>(toml);
220+
assert!(result.is_ok());
221+
let config = result.unwrap();
222+
assert_eq!(config.severity.naming_violation, "error");
223+
}
224+
225+
#[test]
226+
fn test_layer_config_with_name_deny_ignore_parses() {
227+
// name_deny_ignore を含む [[layers]] が parse できる
228+
let toml = r#"
229+
[project]
230+
name = "myproject"
231+
root = "."
232+
languages = ["rust"]
167233
168234
[[layers]]
169235
name = "domain"
170236
paths = ["src/domain/**"]
171-
dependency_mode = "opt-in"
172-
external_mode = "opt-in"
237+
dependency_mode = "opt-out"
238+
external_mode = "opt-out"
239+
name_deny = ["aws", "gcp"]
240+
name_deny_ignore = ["**/test_*.rs", "tests/**"]
173241
"#;
174242
let result = toml::from_str::<MilleConfig>(toml);
175243
assert!(
176244
result.is_ok(),
177-
"src_root ありも parse できるべき: {:?}",
245+
"name_deny_ignore で parse できるべき: {:?}",
178246
result.err()
179247
);
180248
let config = result.unwrap();
181-
let py = config.resolve.unwrap().python.unwrap();
182-
assert_eq!(py.src_root, "src");
249+
assert_eq!(
250+
config.layers[0].name_deny_ignore,
251+
vec!["**/test_*.rs", "tests/**"]
252+
);
183253
}
184254
}

0 commit comments

Comments
 (0)