Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kotlin AvoidRecreatingDateTimeFormatter #304

Merged
merged 8 commits into from
Jun 3, 2024
35 changes: 34 additions & 1 deletion src/main/resources/category/kotlin/common.xml
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,6 @@ object GoodAvoidRecompilingXPathExpressionKt {
<priority>2</priority>
<properties>
<property name="tag" value="jpinpoint-rule" type="String" description="for-sonar"/>
<property name="version" value="3.1"/>
<property name="xpath">
<value><![CDATA[
(: check if java.text imports exists as filter for same named classes in other packages :)
Expand Down Expand Up @@ -292,5 +291,39 @@ class Foo {
</example>
</rule>

<rule name="AvoidRecreatingDateTimeFormatter"
message="Avoid recreating DateTimeFormatter, it is relatively expensive."
class="net.sourceforge.pmd.lang.rule.XPathRule" dfa="false" language="kotlin"
typeResolution="true" externalInfoUrl="https://github.com/jborgers/PMD-jPinpoint-rules/tree/master/docs/JavaCodePerformance.md#idtf02">
<description>
Problem: Recreating a DateTimeFormatter is relatively expensive.&#13;
Solution: org.joda.time.format.DateTimeFormatter or Java 8 java.time.DateTimeFormatter is thread-safe and can be shared among threads. Create the
formatter from a pattern only once, to initialize a constant or instance field.
(jpinpoint-rules)</description>
<priority>2</priority>
<properties>
<property name="tag" value="jpinpoint-rule" type="String" description="for-sonar"/>
<property name="xpath">
<value>
<![CDATA[
(: check if java.text imports exists as filter for same named classes in other packages :)
//ImportHeader[.//T-Identifier[@Text='org'] and .//T-Identifier[@Text='joda'] and .//T-Identifier[@Text='time'] and .//T-Identifier[@Text='format']][1]/ancestor::KotlinFile
stokpop marked this conversation as resolved.
Show resolved Hide resolved

/TopLevelObject//Expression//T-Identifier[
(@Text='DateTimeFormatter' or @Text='DateTimeFormat' or @Text='DateTimeFormatterBuilder' or @Text='ISODateTimeFormat')
(: val is "final" and is allowed in CompanionObject :)
and (not(ancestor::PropertyDeclaration/T-VAL) or (ancestor::PropertyDeclaration/T-VAL and not(ancestor::CompanionObject)))
(: do not match explicit type declarations :)
and not(ancestor::TypeReference)
(: variable params are allowed, e.g. when it is an incoming function param :)
and not(ancestor::PrefixUnaryExpression//ValueArgument[.//T-Identifier[@Text = ancestor::FunctionDeclaration/FunctionValueParameters/FunctionValueParameter[Parameter/Type//T-Identifier[@Text='String']]//T-Identifier/@Text]])
(: variable params are allowed in field init in constructors if val :)
and (not(ancestor::PrefixUnaryExpression//ValueArgument[.//T-Identifier[@Text = ancestor::Declaration//ClassParameter[Type//T-Identifier[@Text='String']]//T-Identifier/@Text] and (ancestor::ClassMemberDeclaration//PropertyDeclaration/T-VAL)]))
]
]]>
</value>
</property>
</properties>
</rule>

</ruleset>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.jpinpoint.perf.lang.kotlin.ruleset.common;

import net.sourceforge.pmd.testframework.PmdRuleTst;

public class AvoidRecreatingDateTimeFormatterTest extends PmdRuleTst {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<test-data
xmlns="http://pmd.sourceforge.net/rule-tests"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests http://pmd.sourceforge.net/rule-tests_1_0_0.xsd">
<test-code>
<description>Avoid recreation of DateTimeFormatter object.</description>
<expected-problems>13</expected-problems>
<expected-linenumbers>8,14,15,18,22,26,30,34,38,43,55,64,68</expected-linenumbers>
<code><![CDATA[
import org.joda.time.format.*
import org.springframework.format.annotation.DateTimeFormat
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder

class Foo {
companion object {
var wrongAgain: DateTimeFormatter = ISODateTimeFormat.basicDate() // bad, not final
val ok1: DateTimeFormatter = DateTimeFormat.forPattern("xxx")
private val ok2: DateTimeFormatter = DateTimeFormat.forPattern("xxx")
val YMD_FORMATTER: DateTimeFormatter = org.threeten.bp.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")
}

val wrong: DateTimeFormatter = ISODateTimeFormat.basicDate() // bad, not static
var stillWrong: DateTimeFormatter = ISODateTimeFormat.basicDate() // bad, not final, not static
fun testViolation1(printer: DateTimePrinter?, parser: DateTimeParser?) {
var dtf: DateTimeFormatter? = null
dtf = DateTimeFormatter(printer, parser) // bad
}

fun testViolation2() {
val dtf = DateTimeFormatterBuilder().toFormatter() // bad
}

fun testViolation3() {
val dtf: DateTimeFormatter = ISODateTimeFormat.date() // bad
}

fun testViolation4() {
stillWrong = DateTimeFormat.fullDateTime() // bad
}

fun testViolation5() {
stillWrong = DateTimeFormat.forPattern("") // bad
}

fun testViolation6(printer: DateTimePrinter?, parser: DateTimeParser?) {
stillWrong = DateTimeFormatter(printer, parser) // bad
}

fun testViolation7_PCC_171() {
val dt = 1L
val s: String = DateTimeFormat.forPattern("yyDDD").print(dt) // bad
}

fun testNoViolation(): DateTimeFormatter? {
return null
}

fun testNoViolation_JPCC_16(dateFormat: String) {
val dateTimeFormatter = getDateTimeFormatterFromCacheViolation(dateFormat)
}

private fun getDateTimeFormatterFromCacheViolation(dateFormat: String): DateTimeFormatter {
return DateTimeFormatterBuilder().toFormatter() // bad
}

fun testNoViolation_2(dateFormat: String?) {
val dateTimeFormatter: DateTimeFormatter =
DateTimeFormat.forPattern(dateFormat) // good: possible different dataFormat params
}

fun testViolationThreeten() {
val ftor: DateTimeFormatter = org.threeten.bp.format.DateTimeFormatter.ofPattern("yyMMdd") // bad
}

fun testViolationJava8() {
val ftor = DateTimeFormatter.ofPattern("yyMMdd") // bad
}
}
]]></code>
</test-code>
<test-code>
<description>Avoid recreation of DateTimeFormatter object. But allow in constructor if member variable is val.</description>
<expected-problems>3</expected-problems>
<expected-linenumbers>13,14,15</expected-linenumbers>
<code><![CDATA[
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.DateTimeFormatter
import org.joda.time.format.DateTimeFormatterBuilder

class Foo(pattern: String?) {

companion object {
private val ok2 = DateTimeFormat.forPattern("YY-MM-hh")
}

private val ok: DateTimeFormatter = DateTimeFormat.forPattern(pattern)
private val notok: DateTimeFormatter =
DateTimeFormat.forPattern("YY-MM-hh") // bad
private val notok2: DateTimeFormatter = DateTimeFormat.forPattern("YY-MM-hh") // bad (should be in companion object)
private var notok3 = DateTimeFormatterBuilder().appendPattern(pattern).toFormatter() // bad (should be val)
}
]]></code>
</test-code>
<test-code>
<description>Avoid recreation of DateTimeFormatter object. But allow if there is a parameter from method call involved.</description>
<expected-problems>3</expected-problems>
<expected-linenumbers>8,9,10</expected-linenumbers>
<code><![CDATA[
import org.joda.time.format.DateTimeFormat
import java.time.ZoneId

object Foo {
private const val DEFAULT_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
fun format(milliseconds: Long, myFormat: String?, myTimeZone: String?): String {
val formatter1 = DateTimeFormat.forPattern(myFormat) // good: has variable parameter
val formatter2 = DateTimeFormat.forPattern(DEFAULT_DATETIME_FORMAT) // bad
val formatter3 = DateTimeFormat.forPattern(DEFAULT_DATETIME_FORMAT).withZoneUTC() // bad
val formatter4 = DateTimeFormat.forPattern(DEFAULT_DATETIME_FORMAT).withZone(ZoneId.of("UTC")) // bad
val formatter5 =
DateTimeFormat.forPattern(DEFAULT_DATETIME_FORMAT).withZone(myTimeZone) // good: has variable parameter
return formatter1.print(milliseconds)
}
}
]]></code>
</test-code>
stokpop marked this conversation as resolved.
Show resolved Hide resolved
</test-data>