Skip to content

Commit eb6f659

Browse files
authored
Merge pull request #1 from NordCoderd/reversed-package-rules
1.0.7 added many rules and supression mechanism
2 parents f924e7a + 5d1e138 commit eb6f659

72 files changed

Lines changed: 1778 additions & 10 deletions

File tree

Some content is hidden

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

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,6 @@ bin/
4545

4646
### Mac OS ###
4747
.DS_Store
48+
.grepai/
49+
.claude
50+
konsist-sources/

AGENTS.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Spring Boot Code Guard
2+
3+
Konsist-based static analysis library for Spring Boot. Rules run as JUnit 5 tests. Kotlin + Gradle (Kotlin DSL), JVM 17. Published to Maven Central as `dev.protsenko:spring-boot-code-guard`.
4+
5+
## Tools (mandatory)
6+
7+
- **File & coding tasks**: use the **mcpc idea** tools (skill: /skill:mcpc-idea-strict-workflow) for every read, edit, create, rename, reformat, inspection, build, and run operation — do not use generic shell fallbacks when an mcpc @idea equivalent exists.
8+
- **Project lookup (when mcpc failed)**: use the **GrepAI** skills (`grepai search`, `grepai trace callers|callees|graph`, `grepai status`) for all code search, symbol discovery, and call-graph questions.
9+
10+
## Layout
11+
12+
```
13+
src/main/kotlin/dev/protsenko/codeguard/
14+
core/ # SpringBootRule, DSL entry, suppression, RuleBuilder
15+
rules/{general,jpa,naming,packages,web}/ # rule objects + DSL contexts
16+
src/test/kotlin/
17+
dev/protsenko/codeguard/coverage/ # violation tests per rule group
18+
fixtures/violations/ # fixture classes triggered by tests
19+
```
20+
21+
Entry point: `springBootRules { }.verify()` (`core/SpringBootRulesConfiguration.kt`).
22+
23+
## Rule anatomy
24+
25+
Each rule is an `object : SpringBootRule` with `description`, `suppressKey`, `verify(scope)`. Per-class silence: `@Suppress("CodeGuard:<key>")`. DSL-wide opt-out: `exclude("CodeGuard:<key>")`.
26+
27+
## Adding a new rule — mandatory checklist
28+
29+
Follow every step in order. Do not skip.
30+
31+
### 1. TDD — fixtures and tests first (use `/simplify` skill after)
32+
33+
Use `/skill:mcpc-idea-strict-workflow` to create fixture classes under `src/test/kotlin/fixtures/violations/<category>/<rule>/`:
34+
- `*Negative.kt` — class that triggers the violation (one per interesting failure case)
35+
- `*Positive.kt` — class that must pass (one per interesting pass case)
36+
37+
Then add test methods to the matching `*ViolationTest.kt` in `src/test/kotlin/dev/protsenko/codeguard/coverage/`. Each negative test must assert the exact error message. Each positive test just calls `rule.verify(scope)` without `assertFailsWith`.
38+
39+
Typical cases to cover per rule:
40+
- Wrong stereotype alone in the constrained location → fail
41+
- Correct stereotype alone → pass
42+
- Correct stereotype with file-level helpers in same file → pass
43+
- Unannotated class alone in constrained location → fail
44+
45+
### 2. Implement the rule
46+
47+
Add the rule `object` to the relevant `*Rules.kt` in `src/main/kotlin/.../rules/<category>/`. Use `hasAnnotationWithName(SpringAnnotations.*)` (not `hasAnnotationOf`). Group by `containingFile` when file-level helper exemptions are needed.
48+
49+
### 3. Register in `all*Rules` list
50+
51+
Every `*Rules.kt` file exposes a `val all*Rules: List<SpringBootRule>` at the bottom. Add the new rule there. `AllRulesTest` asserts exact counts — update `allPackageRules contains N rules` (or equivalent) to match.
52+
53+
### 4. Expose in DSL context and `AllRulesTest` individual block
54+
55+
Add a DSL function to the matching `*RuleContext.kt`. Then add the call to the `withIndividual` block in `AllRulesTest.kt` alongside existing peers (e.g. `entitiesInEntityPackage()`).
56+
57+
### 5. Register in `UsageExampleTest`
58+
59+
Add the new DSL call next to its category peers in `src/test/kotlin/dev/protsenko/codeguard/usage/UsageExampleTest.kt`.
60+
61+
### 6. Update README.md
62+
63+
Add one bullet to the matching section in `README.md` under `## Rule Set`. Format: `` - `CodeGuard:<key>`: <what it enforces and why>. Exception: <if any>. ``
64+
65+
### 7. Run `./gradlew codeBaseline`
66+
67+
Must pass clean: tests + detekt + 90% coverage floor.

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Maven Central: https://central.sonatype.com/artifact/dev.protsenko/spring-boot-c
1111
Add dependency to your `build.gradle.kts` or `build.gradle`:
1212

1313
```kotlin
14-
implementation("dev.protsenko:spring-boot-code-guard:1.0.4")
14+
implementation("dev.protsenko:spring-boot-code-guard:1.0.7")
1515
```
1616

1717
## Usage
@@ -150,6 +150,13 @@ Passing an unknown key throws an error listing all registered rule keys. Excludi
150150
- `CodeGuard:propertiesValidation`: `@ConfigurationProperties` classes must reside in a `..property..` package segment.
151151
- `CodeGuard:configurationPropertiesPrefixKebabCase`: `@ConfigurationProperties` prefixes must use lowercase kebab-case segments separated by dots, such as `app.mail` or `app-mail.client`.
152152
- `CodeGuard:entityPackage`: `@Entity` classes must reside in a `..domain..` or `..entity..` package segment.
153+
- `CodeGuard:onlyServicesInServicePackage`: Only `@Service` classes may reside in a `..service..` package segment — other stereotypes (`@Component`, `@Repository`, etc.) and plain classes must not be placed there. Exception: non-`@Service` classes declared in the same file as a `@Service` class are treated as file-level helpers and are permitted; nested helper classes are ignored.
154+
- `CodeGuard:onlyEntitiesInEntityPackage`: Only `@Entity` classes may reside in `..domain..` or `..entity..` package segments — non-entity stereotypes and plain classes must not be placed there. Exception: JPA `@MappedSuperclass` and `@Embeddable` types are also permitted, classes referenced from an entity's `@IdClass` are permitted, and non-`@Entity` classes declared in the same file as an `@Entity` class or a class inheriting from `@Entity` are treated as file-level helpers and are permitted.
155+
- `CodeGuard:onlyControllersInControllerPackage`: Only `@Controller`/`@RestController` classes may reside in `..controller..` or `..web..` package segments — other stereotypes and plain classes must not be placed there. Exception: non-controller classes declared in the same file as a controller are treated as file-level helpers and are permitted.
156+
- `CodeGuard:onlyConfigurationsInConfigPackage`: Only `@Configuration`, `@ControllerAdvice`, and `@RestControllerAdvice` classes may reside in `..config..` or `..configuration..` package segments — other stereotypes and plain classes must not be placed there. Exception: other classes declared in the same file as one of these allowed types are treated as file-level helpers and are permitted.
157+
- `CodeGuard:onlyPropertiesInPropertyPackage`: Only `@ConfigurationProperties` classes may reside in `..property..` package segments — other stereotypes and plain classes must not be placed there. Exception: non-`@ConfigurationProperties` classes declared in the same file as a `@ConfigurationProperties` class are treated as file-level helpers and are permitted.
158+
- `CodeGuard:repositoryPackage`: `@Repository` classes and Spring Data repository interfaces (e.g. extending `JpaRepository`) must reside in a `..repository..` package segment.
159+
- `CodeGuard:onlyRepositoriesInRepositoryPackage`: Only `@Repository` classes may reside in `..repository..` package segments — other stereotypes and plain classes must not be placed there. Exception: non-`@Repository` classes declared in the same file as a `@Repository` class are treated as file-level helpers and are permitted.
153160

154161
### Web
155162

build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import com.vanniktech.maven.publish.JavadocJar
22
import com.vanniktech.maven.publish.SourcesJar
3+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
34

45
plugins {
56
kotlin("jvm") version "2.3.0"
@@ -10,7 +11,7 @@ plugins {
1011
}
1112

1213
group = "dev.protsenko"
13-
version = "1.0.5"
14+
version = "1.0.7"
1415

1516
repositories {
1617
mavenCentral()
@@ -38,6 +39,13 @@ detekt {
3839
config.setFrom(files("detekt.yml"))
3940
}
4041

42+
tasks.withType<KotlinCompile>().configureEach {
43+
compilerOptions {
44+
allWarningsAsErrors.set(true)
45+
}
46+
}
47+
48+
4149
kover {
4250
reports {
4351
verify {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#Sun Mar 08 00:12:25 AMT 2026
22
distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
4-
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

src/main/kotlin/dev/protsenko/codeguard/rules/SpringAnnotations.kt

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package dev.protsenko.codeguard.rules
22

3+
import com.lemonappdev.konsist.api.declaration.combined.KoClassAndInterfaceDeclaration
4+
import com.lemonappdev.konsist.api.provider.KoFullyQualifiedNameProvider
5+
36
/**
47
* Fully-qualified annotation names and composite groups shared across all rule implementations.
58
*/
@@ -36,6 +39,12 @@ object SpringAnnotations {
3639
// ---- JPA ----
3740
const val ENTITY_JAKARTA = "jakarta.persistence.Entity"
3841
const val ENTITY_JAVAX = "javax.persistence.Entity"
42+
const val MAPPED_SUPERCLASS_JAKARTA = "jakarta.persistence.MappedSuperclass"
43+
const val MAPPED_SUPERCLASS_JAVAX = "javax.persistence.MappedSuperclass"
44+
const val EMBEDDABLE_JAKARTA = "jakarta.persistence.Embeddable"
45+
const val EMBEDDABLE_JAVAX = "javax.persistence.Embeddable"
46+
const val ID_CLASS_JAKARTA = "jakarta.persistence.IdClass"
47+
const val ID_CLASS_JAVAX = "javax.persistence.IdClass"
3948
const val ID_JAKARTA = "jakarta.persistence.Id"
4049
const val ID_JAVAX = "javax.persistence.Id"
4150

@@ -64,9 +73,26 @@ object SpringAnnotations {
6473
/** @Entity (jakarta + javax). */
6574
val entityAnnotations = listOf(ENTITY_JAKARTA, ENTITY_JAVAX)
6675

76+
/** JPA types allowed in entity/domain packages. */
77+
val entityPackageAnnotations =
78+
listOf(
79+
ENTITY_JAKARTA,
80+
ENTITY_JAVAX,
81+
MAPPED_SUPERCLASS_JAKARTA,
82+
MAPPED_SUPERCLASS_JAVAX,
83+
EMBEDDABLE_JAKARTA,
84+
EMBEDDABLE_JAVAX,
85+
)
86+
6787
/** @Id (jakarta + javax). */
6888
val idAnnotations = listOf(ID_JAKARTA, ID_JAVAX)
6989

90+
/** @IdClass (jakarta + javax). */
91+
val idClassAnnotations = listOf(ID_CLASS_JAKARTA, ID_CLASS_JAVAX)
92+
93+
/** Types allowed in config/configuration packages. */
94+
val configurationPackageAnnotations = listOf(CONFIGURATION, CONTROLLER_ADVICE, REST_CONTROLLER_ADVICE)
95+
7096
/** All @Transactional variants. */
7197
val transactionalAnnotations = listOf(TRANSACTIONAL, TRANSACTIONAL_JAKARTA, TRANSACTIONAL_JAVAX)
7298

@@ -96,8 +122,19 @@ object SpringAnnotations {
96122
/** Annotations that rely on Spring AOP proxy and are silently ignored on private methods. */
97123
val proxyAnnotations =
98124
listOf(
99-
TRANSACTIONAL, TRANSACTIONAL_JAKARTA, TRANSACTIONAL_JAVAX,
100-
CACHEABLE, CACHE_EVICT, CACHE_PUT,
125+
TRANSACTIONAL,
126+
TRANSACTIONAL_JAKARTA,
127+
TRANSACTIONAL_JAVAX,
128+
CACHEABLE,
129+
CACHE_EVICT,
130+
CACHE_PUT,
101131
ASYNC,
102132
)
103133
}
134+
135+
fun KoClassAndInterfaceDeclaration.isSpringDataRepository(): Boolean =
136+
hasAnnotationWithName(SpringAnnotations.REPOSITORY) ||
137+
hasExternalParent { parent ->
138+
val fqn = (parent.sourceDeclaration as? KoFullyQualifiedNameProvider)?.fullyQualifiedName
139+
fqn != null && fqn.startsWith("org.springframework.data") && fqn.contains(".repository.")
140+
}

src/main/kotlin/dev/protsenko/codeguard/rules/naming/NamingRules.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import dev.protsenko.codeguard.core.SpringBootRule
66
import dev.protsenko.codeguard.core.notSuppressedClasses
77
import dev.protsenko.codeguard.core.notSuppressedClassesAndInterfaces
88
import dev.protsenko.codeguard.rules.SpringAnnotations
9+
import dev.protsenko.codeguard.rules.isSpringDataRepository
910

1011
/**
1112
* Rules for naming conventions across Spring Boot layers.
@@ -46,7 +47,7 @@ object NamingRules {
4647
override fun verify(scope: KoScope) {
4748
scope
4849
.notSuppressedClassesAndInterfaces(suppressKey)
49-
.withAnnotationNamed(SpringAnnotations.REPOSITORY)
50+
.filter { it.isSpringDataRepository() }
5051
.filterNot { it.name.endsWith("Repository") }
5152
.also { violations ->
5253
if (violations.isNotEmpty()) {

src/main/kotlin/dev/protsenko/codeguard/rules/packages/PackageRuleContext.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,53 @@ class PackageRuleContext : RuleContext() {
5656
fun entitiesInEntityPackage() {
5757
builder.addRule(PackageRules.entityPackageRule)
5858
}
59+
60+
/**
61+
* Enforce that only @Service classes (or their file-level helpers) reside in .service package.
62+
*/
63+
fun onlyServicesInServicePackage() {
64+
builder.addRule(PackageRules.onlyServicesInServicePackageRule)
65+
}
66+
67+
/**
68+
* Enforce that only @Entity classes (or their file-level helpers) reside in .domain or .entity package.
69+
*/
70+
fun onlyEntitiesInEntityPackage() {
71+
builder.addRule(PackageRules.onlyEntitiesInEntityPackageRule)
72+
}
73+
74+
/**
75+
* Enforce that only @Controller/@RestController classes (or their file-level helpers) reside in .controller or .web package.
76+
*/
77+
fun onlyControllersInControllerPackage() {
78+
builder.addRule(PackageRules.onlyControllersInControllerPackageRule)
79+
}
80+
81+
/**
82+
* Enforce that only @Configuration classes (or their file-level helpers) reside in .config or .configuration package.
83+
*/
84+
fun onlyConfigurationsInConfigPackage() {
85+
builder.addRule(PackageRules.onlyConfigurationsInConfigPackageRule)
86+
}
87+
88+
/**
89+
* Enforce that only @ConfigurationProperties classes (or their file-level helpers) reside in .property package.
90+
*/
91+
fun onlyPropertiesInPropertyPackage() {
92+
builder.addRule(PackageRules.onlyPropertiesInPropertyPackageRule)
93+
}
94+
95+
/**
96+
* Enforce that @Repository classes and Spring Data repository interfaces reside in .repository package.
97+
*/
98+
fun repositoryInRepositoryPackage() {
99+
builder.addRule(PackageRules.repositoryPackageRule)
100+
}
101+
102+
/**
103+
* Enforce that only @Repository classes (or their file-level helpers) reside in .repository package.
104+
*/
105+
fun onlyRepositoriesInRepositoryPackage() {
106+
builder.addRule(PackageRules.onlyRepositoriesInRepositoryPackageRule)
107+
}
59108
}

0 commit comments

Comments
 (0)