Skip to content

Commit

Permalink
Add naming rules for packages, classes, objects, functions and proper…
Browse files Browse the repository at this point in the history
…ties

Closes pinterest#44
  • Loading branch information
paul-dingemans committed Nov 6, 2022
1 parent 65b722c commit 9a73338
Show file tree
Hide file tree
Showing 13 changed files with 692 additions and 1 deletion.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ if (node.isRoot()) {

### Added
* Wrap blocks in case the max line length is exceeded or in case the block contains a new line `wrapping` ([#1643](https://github.com/pinterest/ktlint/issue/1643))

* patterns can be read in from `stdin` with the `--patterns-from-stdin` command line options/flags ([#1606](https://github.com/pinterest/ktlint/pull/1606))
* Add basic formatting for context receiver in `indent` rule and new experimental rule `context-receiver-wrapping` ([#1672](https://github.com/pinterest/ktlint/issue/1672))
* Add naming rules for packages (`package-naming`), classes (`class-naming`), objects (`object-naming`), functions (`function-naming`) and properties (`property-naming`) ([#44](https://github.com/pinterest/ktlint/issue/44))

### Fixed

Expand Down
32 changes: 32 additions & 0 deletions docs/rules/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,38 @@ Rewrites the function signature to a single line when possible (e.g. when not ex

Rule id: `function-signature`

## Naming

### Class naming

Enforce naming of class.

Rule id: `experimental:class-naming`

### Function naming

Enforce naming of function.

Rule id: `experimental:function-naming`

### Object naming

Enforce naming of object.

Rule id: `experimental:object-naming`

### Package naming

Enforce naming of package.

Rule id: `experimental:package-naming`

### Property naming

Enforce naming of property.

Rule id: `experimental:property-naming`

## Spacing

### Fun keyword spacing
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CLASS
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* https://kotlinlang.org/docs/coding-conventions.html#naming-rules
*/
public class ClassNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:class-naming") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
node
.takeIf { node.elementType == CLASS }
?.findChildByType(IDENTIFIER)
?.takeUnless { it.text.matches(VALID_CLASS_NAME_REGEXP) }
?.let {
emit(it.startOffset, "Class name should start with an uppercase letter and use camel case", false)
}
}

private companion object {
val VALID_CLASS_NAME_REGEXP = Regex("[A-Z][a-zA-Z0-9]*")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public class ExperimentalRuleSetProvider :
NullableTypeSpacingRule(),
FunctionSignatureRule(),
ContextReceiverWrappingRule(),
ClassNamingRule(),
FunctionNamingRule(),
ObjectNamingRule(),
PackageNamingRule(),
PropertyNamingRule(),
)

override fun getRuleProviders(): Set<RuleProvider> =
Expand All @@ -63,5 +68,10 @@ public class ExperimentalRuleSetProvider :
RuleProvider { NullableTypeSpacingRule() },
RuleProvider { FunctionSignatureRule() },
RuleProvider { ContextReceiverWrappingRule() },
RuleProvider { ClassNamingRule() },
RuleProvider { FunctionNamingRule() },
RuleProvider { ObjectNamingRule() },
RuleProvider { PackageNamingRule() },
RuleProvider { PropertyNamingRule() },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE
import com.pinterest.ktlint.core.ast.children
import com.pinterest.ktlint.core.ast.nextLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* https://kotlinlang.org/docs/coding-conventions.html#function-names
*/
public class FunctionNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:function-naming") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
node
.takeIf { node.elementType == FUN }
?.takeUnless {
node.isFactoryMethod() ||
node.isTestMethod() ||
node.hasValidFunctionName()
}?.let {
val identifierOffset =
node
.findChildByType(IDENTIFIER)
?.startOffset
?: 1
emit(identifierOffset, "Function name should start with a lowercase letter (except factory methods) and use camel case", false)
}
}

private fun ASTNode.isFactoryMethod() =
findChildByType(TYPE_REFERENCE)?.text == findChildByType(IDENTIFIER)?.text

private fun ASTNode.isTestMethod() =
hasBackTickedIdentifier() && hasTestAnnotation()

private fun ASTNode.hasBackTickedIdentifier() =
findChildByType(IDENTIFIER)
?.text
.orEmpty()
.matches(BACK_TICKED_FUNCTION_NAME_REGEXP)

private fun ASTNode.hasTestAnnotation() =
findChildByType(MODIFIER_LIST)
?.children()
.orEmpty()
.any { it.hasAnnotationWithIdentifierEndingWithTest() }

private fun ASTNode.hasAnnotationWithIdentifierEndingWithTest() =
elementType == ANNOTATION_ENTRY &&
nextLeaf { it.elementType == IDENTIFIER }
?.text
.orEmpty()
.endsWith("Test")

private fun ASTNode.hasValidFunctionName() =
findChildByType(IDENTIFIER)
?.text
.orEmpty()
.matches(VALID_FUNCTION_NAME_REGEXP)

private companion object {
val VALID_FUNCTION_NAME_REGEXP = Regex("[a-z][a-zA-Z0-9]*")
val BACK_TICKED_FUNCTION_NAME_REGEXP = Regex("`.*`")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* https://kotlinlang.org/docs/coding-conventions.html#naming-rules
*/
public class ObjectNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:object-naming") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
node
.takeIf { node.elementType == OBJECT_DECLARATION }
?.findChildByType(IDENTIFIER)
?.takeUnless { it.text.matches(VALID_OBJECT_NAME_REGEXP) }
?.let {
emit(it.startOffset, "Object name should start with an uppercase letter and use camel case", false)
}
}

private companion object {
val VALID_OBJECT_NAME_REGEXP = Regex("[A-Z][a-zA-Z]*")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
import com.pinterest.ktlint.core.ast.nextCodeSibling
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* https://kotlinlang.org/docs/coding-conventions.html#naming-rules
*/
public class PackageNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:package-naming") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
node
.takeIf { node.elementType == PACKAGE_DIRECTIVE }
?.firstChildNode
?.nextCodeSibling()
?.takeIf { it.elementType == DOT_QUALIFIED_EXPRESSION || it.elementType == REFERENCE_EXPRESSION }
?.takeUnless { it.text.matches(VALID_PACKAGE_NAME_REGEXP) }
?.let {
emit(it.startOffset, "Package name should contain lowercase characters only", false)
}
}

private companion object {
val VALID_PACKAGE_NAME_REGEXP = Regex("[a-z]+(\\.[a-z]+)*")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY
import com.pinterest.ktlint.core.ast.ElementType.CONST_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.FILE
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
import com.pinterest.ktlint.core.ast.ElementType.OBJECT_DECLARATION
import com.pinterest.ktlint.core.ast.ElementType.OVERRIDE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.PRIVATE_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.PROPERTY
import com.pinterest.ktlint.core.ast.ElementType.PROPERTY_ACCESSOR
import com.pinterest.ktlint.core.ast.ElementType.VAL_KEYWORD
import com.pinterest.ktlint.core.ast.children
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType

/**
* https://kotlinlang.org/docs/coding-conventions.html#function-names
* https://kotlinlang.org/docs/coding-conventions.html#property-names
*/
public class PropertyNamingRule : Rule("$EXPERIMENTAL_RULE_SET_ID:property-naming") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
node
.takeIf { node.elementType == PROPERTY }
?.let { property -> visitProperty(property, emit) }
}

private fun visitProperty(
property: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
property
.findChildByType(IDENTIFIER)
?.let { identifier ->
when {
property.hasCustomGetter() -> {
// Can not reliably determine whether the value is immutable or not
}
property.isBackingProperty() -> {
visitBackingProperty(identifier, emit)
}
property.hasConstModifier() ||
property.isTopLevelValue() ||
property.isObjectValue() -> {
visitConstProperty(identifier, emit)
}
else -> {
visitNonConstProperty(identifier, emit)
}
}
}
}

private fun visitBackingProperty(
identifier: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
identifier
.text
.takeUnless { it.matches(BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP) }
?.let {
emit(identifier.startOffset, "Backing property name should start with underscore followed by lower camel case", false)
}
}

private fun visitConstProperty(
identifier: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
identifier
.text
.takeUnless { it.matches(SCREAMING_SNAKE_CASE_REGEXP) }
?.let {
emit(identifier.startOffset, "Property name should use the screaming snake case notation when the value can not be changed", false)
}
}

private fun visitNonConstProperty(
identifier: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
identifier
.text
.takeUnless { it.matches(LOWER_CAMEL_CASE_REGEXP) }
?.let {
emit(identifier.startOffset, "Property name should start with a lowercase letter and use camel case", false)
}
}

private fun ASTNode.hasCustomGetter() =
findChildByType(PROPERTY_ACCESSOR)
?.firstChildNode
?.text == "get"

private fun ASTNode.hasConstModifier() =
hasModifier(CONST_KEYWORD)

private fun ASTNode.hasModifier(iElementType: IElementType) =
findChildByType(MODIFIER_LIST)
?.children()
.orEmpty()
.any { it.elementType == iElementType }

private fun ASTNode.isTopLevelValue() =
treeParent.elementType == FILE && containsValKeyword()

private fun ASTNode.containsValKeyword() =
children().any { it.elementType == VAL_KEYWORD }

private fun ASTNode.isObjectValue() =
treeParent.elementType == CLASS_BODY &&
treeParent?.treeParent?.elementType == OBJECT_DECLARATION &&
containsValKeyword() &&
!hasModifier(OVERRIDE_KEYWORD)

private fun ASTNode.isBackingProperty() =
takeIf { hasModifier(PRIVATE_KEYWORD) }
?.findChildByType(IDENTIFIER)
?.takeIf { it.text.startsWith("_") }
?.let { identifier ->
this.hasPublicProperty(identifier.text.removePrefix("_"))
}
?: false

private fun ASTNode.hasPublicProperty(identifier: String) =
treeParent
.children()
.filter { it.elementType == PROPERTY }
.mapNotNull { it.findChildByType(IDENTIFIER) }
.any { it.text == identifier }

private companion object {
val LOWER_CAMEL_CASE_REGEXP = Regex("[a-z][a-zA-Z0-9]*")
val SCREAMING_SNAKE_CASE_REGEXP = Regex("[A-Z][_A-Z0-9]*")
val BACKING_PROPERTY_LOWER_CAMEL_CASE_REGEXP = Regex("_[a-z][a-zA-Z0-9]*")
}
}
Loading

0 comments on commit 9a73338

Please # to comment.