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

AvoidStringBuffer kotlin rule #292

Merged
merged 15 commits into from Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/EvaluateRulesForKotlin.md
Expand Up @@ -9,14 +9,14 @@ Evaluation of Java PMD rules for use as Kotlin rules
| 3. | PP-1 | AvoidDecimalAndChoiceFormatAsField | Yes? | Low | Yes | High | Not found | NumberFornat/DateFormat not included? |
| 4. | | AvoidDuplicateAssignmentsInCases | Yes | Medium | Yes | Low/Medium | Partly found | Detekt:DuplicateCaseInWhenExpression has overlap but is not the same. Add example, doc, Questionable if occuring often. |
| 5. | | AvoidImplicitlyRecompilingRegex | Yes | High | Yes | High | Not found | Kotlin has own String/regex, also occurs here? support both? |
| 6. | JB-1 | AvoidInMemoryStreamingDefaultConstructor | Yes | Low | Yes | High | Not found | Kotlin types? -> No |
| 6. | JB-1-PR | AvoidInMemoryStreamingDefaultConstructor | Yes | Low | Yes | High | Not found | Kotlin types? -> No |
| 7. | | AvoidMultipleConcatStatements | Yes | Medium | Yes | High | Not found | How concat in Kotlin? Seems like Java |
| 8. | | AvoidRecompilingPatterns | Yes | Low/Medium | Yes | High | Not found | Kotlin version? |
| 9. | | AvoidRecompilingXPathExpression | Yes | Low | Yes | Medium/High | Not found | Good example ThreadLocal in Kotlin? |
| 8. | PP-3 | AvoidRecompilingPatterns | Yes | Low/Medium | Yes | High | Not found | Kotlin version? |
| 9. | JB-3 | AvoidRecompilingXPathExpression | Yes | Low | Yes | Medium/High | Not found | Good example ThreadLocal in Kotlin? |
| 10. | | AvoidRecreatingDateTimeFormatter | Yes | Medium | Yes | High | Not found | - |
| 11. | | AvoidReflectionInToStringAndHashCode | Yes | Low/Medium | Yes | Low/Medium | Not found | - |
| 12. | | AvoidSimpleDateFormat | Yes | Low | Yes | Medium | Not found | |
| 13. | | AvoidStringBuffer | Yes | Low | Yes | Low/Medium | Not found | |
| 12. | PP-2 | AvoidSimpleDateFormat | Yes | Low | Yes | Medium | Not found | |
| 13. | JB-2-PR | AvoidStringBuffer | Yes | Low | Yes | Low/Medium | Not found | |
| 14. | | AvoidUnconditionalBuiltLogStrings | Yes | High | Yes | Medium | Not found | |
| 15. | | AvoidWideScopeXPathExpression | Yes | Low | Yes | Medium | Not found | |
| 16. | | AvoidXPathAPIUsage | Yes | Low | Yes | Medium | Not found | remove VTD reference?, seems old, better alternatives? |
Expand Down
29 changes: 20 additions & 9 deletions docs/JavaCodePerformance.md
Expand Up @@ -829,36 +829,47 @@ See for the example good2: [custom-thread-pool-in-parallel-stream](https://stack

#### IA12
**Observation: Project Reactor Flux.parallel().runOn() is used.**
**Problem:** The data is divided on a number of 'rails' matching the number of CPU cores.
This is only useful in case much CPU processing is performed: if the sequential form takes more than 0,1 ms of CPU time.
**Problem:** The data is divided on a number of 'rails' matching the number of CPU cores (by default).
This is only the proper solution in case much CPU processing is performed: if the sequential form of the CPU work takes more than 0,1 ms of CPU time.
With remote calls this is usually not the case. In addition, it introduces more complexity with risk of failures.
**Solution:** Remove parallel().runOn. For pure CPU processing: use ordinary sequential streaming unless the work takes more than about 0,1 ms in sequential form and proves to be faster with parallelization.
**Solution:** Remove parallel().runOn. For pure CPU processing: only use parallel().runOn() when the work takes more than about 0,1 ms in sequential form and proves to be faster with parallelization.
So only for large collections and much processing.
**Rule name:** AvoidParallelFlux
**Note:** If the call you do is a blocking (remote) call, you are not fully reactive. Then you still need a thread pool for threads waiting for the response.
This can be achieved by [subscribeOn](https://projectreactor.io/docs/core/release/reference/#_the_subscribeon_method) or publishOn.
**See:**
This can be achieved by wrapping the blocking call, using flatMap and [subscribeOn](https://projectreactor.io/docs/core/release/reference/#_the_subscribeon_method) or publishOn.
**See:**
1. [Project Reactor Schedulers](https://projectreactor.io/docs/core/release/reference/#schedulers)
1. [ParallelFlux for CPU work](https://projectreactor.io/docs/core/release/reference/#advanced-parallelizing-parralelflux)
1. [How to wrap a blocking call](https://projectreactor.io/docs/core/release/reference/#advanced-parallelizing-parralelflux)
1. [How to wrap a blocking call](https://projectreactor.io/docs/core/release/reference/#advanced-parallelizing-parralelflux)
2. [ParallelFlux vs flatMap() for a Blocking I/O task](https://stackoverflow.com/questions/43269275/parallelflux-vs-flatmap-for-a-blocking-i-o-task/43273991#43273991)

```java
import reactor.core.publisher.*;

class FooBad {
public Flux<Account> getResponseAccounts(List<AccountKey> accountKeys, List<FieldName> requestedFields) {
return Flux.fromIterable(accountKeys)
.parallel(schedulerProperties.getParallelism()) //bad
.parallel(schedulerProps.getParallelism()) //bad
.runOn(scheduler)
.flatMap(accountKey -> constructAccountDetails(accountKey, requestedFields))
.sequential();
}
}

class FooGood {
class FooGood_NonBlocking {
public Flux<Account> getResponseAccounts(List<AccountKey> accountKeys, List<FieldName> requestedFields) {
return Flux.fromIterable(accountKeys)
.flatMap(accountKey -> constructAccountDetails(accountKey, requestedFields));
// assumed a non-blocking/async call, with blocking you need e.g. subscribeOn(Schedulers.boundedElastic())
// for a non-blocking/async call, fully reactive
}
}

class FooGood_Blocking {
public Flux<Account> getResponseAccounts(List<AccountKey> accountKeys, List<FieldName> requestedFields) {
return Flux.fromIterable(accountKeys)
.flatMap(accountKey -> Mono.fromCallable(() -> {constructAccountDetails(accountKey, requestedFields); return accountKey;})
.subscribeOn(Schedulers.boundedElastic()), schedulerProps.getParallelism());
// for a blocking (I/O) call you still need a thread pool
}
}
```
Expand Down
17 changes: 9 additions & 8 deletions pom.xml
Expand Up @@ -15,8 +15,8 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>6.0.9</spring.version>
<pmd.version>6.55.0</pmd.version>
<pmd.kotlin.version>7.0.0-SNAPSHOT</pmd.kotlin.version>
<!--pmd.version>6.55.0</pmd.version>
<pmd.kotlin.version>7.0.0-rc4</pmd.kotlin.version-->
</properties>

<build>
Expand Down Expand Up @@ -193,9 +193,9 @@
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<!--properties>
<pmd.version>6.51.0</pmd.version>
</properties-->
<properties>
<pmd.version>6.55.0</pmd.version>
</properties>
<build>
<plugins>
<plugin>
Expand All @@ -212,9 +212,10 @@
</profile>
<profile>
<id>kotlin-pmd7</id>
<!--properties>
<pmd.kotlin.version>7.0.0-SNAPSHOT</pmd.kotlin.version>
</properties!-->
<properties>
<pmd.kotlin.version>7.0.0-rc4</pmd.kotlin.version>
<pmd.version>7.0.0-rc4</pmd.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
Expand Down
9 changes: 5 additions & 4 deletions rulesets/java/jpinpoint-rules.xml
Expand Up @@ -300,9 +300,10 @@ VariableInitializer/Expression/PrimaryExpression/PrimaryPrefix/Literal[string-le
</properties>
</rule>

<rule name="AvoidInMemoryStreamingDefaultConstructor" class="net.sourceforge.pmd.lang.rule.XPathRule" dfa="false" language="java" message="Default capacity or smaller is used for ByteArrayOutputStream or StringWriter, it usually needs expensive expansions." typeResolution="true"
<rule name="AvoidInMemoryStreamingDefaultConstructor" class="net.sourceforge.pmd.lang.rule.XPathRule" dfa="false" language="java"
message="The default capacity or smaller is used for ByteArrayOutputStream or StringWriter, it usually needs expensive expansions." typeResolution="true"
externalInfoUrl="https://github.com/jborgers/PMD-jPinpoint-rules/tree/master/docs/JavaCodePerformance.md#isio01">
<description>The default constructor of ByteArrayOutputStream creates a 32 bytes initial capacity and for StringWriter 16 chars. Problem: Such a small buffer as capacity usually needs several expensive expansions.&#13;
<description>Problem: The default constructor of ByteArrayOutputStream creates a 32 bytes initial capacity and for StringWriter 16 chars. Problem: Such a small buffer as capacity usually needs several expensive expansions.&#13;
Solution: Presize the ByteArrayOutputStream or StringWriter with an initial capacity such that an expansion is not needed in most cases, typically much larger than 32, for instance 4096.
(jpinpoint-rules)</description>
<priority>2</priority>
Expand Down Expand Up @@ -721,12 +722,12 @@ ancestor::MethodDeclaration/MethodDeclarator/FormalParameters/FormalParameter/Va

<rule name="AvoidRecreatingSecurityProviders"
language="java"
message="Avoid re-creating security providers."
message="Avoid re-creating security providers, this is expensive."
class="net.sourceforge.pmd.lang.rule.XPathRule"
typeResolution="true"
externalInfoUrl="https://github.com/jborgers/PMD-jPinpoint-rules/tree/master/docs/JavaCodePerformance.md#iuosf01">
<description>Problem: Creating a security provider is expensive because of loading of algorithms and other classes. Additionally, it uses synchronized which leads to lock contention when used with multiple threads.
Solution: This only needs to happen once in the JVM lifetime, because once loaded, the provider is available from the Security class. Create the security provider only once: only in case it is nog available from the Security class, yet.
Solution: This only needs to happen once in the JVM lifetime, because once loaded, the provider is typically available from the Security class. Create the security provider only once: only in case it is nog available from the Security class, yet.
(jpinpoint-rules)</description>
<priority>2</priority>
<properties>
Expand Down
41 changes: 39 additions & 2 deletions rulesets/kotlin/jpinpoint-kotlin-rules.xml
Expand Up @@ -30,7 +30,10 @@
<property name="xpath">
<value><![CDATA[
//ImportHeader[.//T-Identifier[@Text='java'] and .//T-Identifier[@Text='io']]/ancestor::KotlinFile
jborgers marked this conversation as resolved.
Show resolved Hide resolved
//Expression//T-Identifier[@Text='ByteArrayOutputStream' or @Text='StringWriter']/../../../PostfixUnarySuffix//ValueArguments[not(ValueArgument)]/../../..
//Expression//T-Identifier[(@Text='ByteArrayOutputStream' and ../../../
PostfixUnarySuffix//ValueArguments[not(ValueArgument) or ValueArgument//T-IntegerLiteral[number(@Text)<=32]])
or (@Text='StringWriter' and ../../../
PostfixUnarySuffix//ValueArguments[not(ValueArgument) or ValueArgument//T-IntegerLiteral[number(@Text)<=16]])]
]]>
</value>
</property>
Expand All @@ -43,7 +46,7 @@ class AvoidInMemoryStreamingDefaultConstructor {
fun bad() {
var baos = ByteArrayOutputStream() //bad
val sw = StringWriter() //bad
baos = ByteArrayOutputStream(32) //bad - not larger than default // TODO
baos = ByteArrayOutputStream(32) //bad - not larger than default
}
fun good() {
val baos = ByteArrayOutputStream(8192) // 8 kiB
Expand All @@ -54,6 +57,40 @@ class AvoidInMemoryStreamingDefaultConstructor {
</example>
</rule>

<rule name="AvoidStringBuffer" class="net.sourceforge.pmd.lang.rule.XPathRule"
since="7.0"
language="kotlin"
message="StringBuffer is used. It introduces locking overhead, use StringBuilder."
typeResolution="true"
externalInfoUrl="https://github.com/jborgers/PMD-jPinpoint-rules/tree/master/docs/JavaCodePerformance.md#isu01" >
<description>Problem: StringBuffer introduces locking overhead because it is thread safe. Its thread-safety is rarely needed.&#13;
Solution: Replace StringBuffer by StringBuilder. (jpinpoint-rules)</description>
<priority>3</priority>
<properties>
<property name="tag" value="jpinpoint-rule" type="String" description="for-sonar"/>
<property name="version" value="3.1"/>
<property name="xpath">
<value><![CDATA[
//VariableDeclaration/Type//T-Identifier[@Text='StringBuffer'],
//Declaration//PrimaryExpression//T-Identifier[@Text='StringBuffer']
]]>
</value>
</property>
</properties>
<example>
<![CDATA[
class AvoidStringBuffer {
var fieldSb: StringBuffer? = null // bad
fun bad() {
val sb = StringBuffer() // bad
}
fun good() {
val sb = StringBuilder()
}
]]>
</example>
</rule>

<!-- END Included file 'common.xml' -->
<!-- BEGIN Included file 'remoting.xml' -->
<rule name="AvoidDeprecatedHystrix"
Expand Down
5 changes: 3 additions & 2 deletions src/main/resources/category/java/common.xml
Expand Up @@ -88,8 +88,9 @@ public interface Foo {
message="Avoid using DecimalFormat or ChoiceFormat as field since it is thread-unsafe."
typeResolution="true"
externalInfoUrl="${doc_root}/JavaCodePerformance.md#idtf01">
<description>Problem: java.text.DecimalFormat and java.text.ChoiceFormat are thread-unsafe. The usual solution
is to create a new local one when needed in a method.
<description>Problem: java.text.DecimalFormat and java.text.ChoiceFormat are thread-unsafe.&#13;
Solution: usual solution is to create a new local one when needed in a method.
(jpinpoint-rules)
</description>
<priority>1</priority>
<properties>
Expand Down