Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .agents/skills/perf-review/references/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,14 @@ every call. Fix: `static final Pattern`.

**`String.format` / format-string parsing.** Re-parses and allocates per call. Fix: direct
concatenation, or pre-compute the result. Also watch for locale-dependent formatting crossing the
wire — a correctness issue on top of the perf one.
wire - a correctness issue on top of the perf one.

**Explicit locales and charsets.** The shared [forbidden-apis ban list](../../../../gradle/forbiddenApiFilters/main.txt)
rejects JDK calls that use an implicit locale or charset. Use `Locale.ROOT` for identifiers and
machine-readable formatting, and the protocol's charset (usually `StandardCharsets.UTF_8`) for
byte conversions. For intentional JVM defaults, use an explicit default or put `@SuppressForbidden`
on the smallest scope and explain why in a comment. Passing a locale still leaves `String.format`
with the parsing and allocation cost described above.

**`Config.get()` per call.** Walks a config-resolution chain; not a free read. Fix: hoist to a
`static final` field at class initialization. This is the single most recurring DBM finding —
Expand Down
12 changes: 12 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ Wildcard imports (`import x.*` or `import static x.*`) are disallowed — see th
* Switch to `IntelliJ IDEA CE 2021.2.3`.
</details>

## Explicit locales and charsets

The shared [forbidden-apis ban list](gradle/forbiddenApiFilters/main.txt) requires explicit
locales and charsets in checked source sets. Use `Locale.ROOT` for identifiers and
machine-readable formatting. Use the protocol's charset, usually `StandardCharsets.UTF_8`,
for byte conversions.

When a JVM default is intentional, pass `Locale.getDefault(...)` or `Charset.defaultCharset()`
explicitly and explain the reason. If the API has no suitable explicit overload, annotate the
smallest enclosing method or constructor with `@SuppressForbidden` and add a comment explaining
why it must use the default. The annotation is a compile-only dependency supplied by the convention.

## Pull request guidelines

### Draft first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,100 @@ class ForbiddenApisConventionPluginTest : GradleFixture() {
.describedAs(result.output).isEqualTo(TaskOutcome.SUCCESS)
}

@Test
fun `requires explicit locales and charsets and permits justified defaults`() {
writeFile("gradle/forbiddenApiFilters/main.txt", File("../gradle/forbiddenApiFilters/main.txt").readText())
writeRootProject(
"""
plugins {
id("dd-trace-java.conventions.forbidden-apis")
}
repositories {
mavenCentral()
}
"""
)
val implicit = """
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.NumberFormat;
import de.thetaphi.forbiddenapis.SuppressForbidden;

public class Example {
public void defaults(byte[] bytes, InputStream input, OutputStream output) throws Exception {
"IDENTIFIER".toLowerCase();
"identifier".toUpperCase();
String.format("%d", 123);
"text".getBytes();
new String(bytes);
new String(bytes, 0, bytes.length);
new InputStreamReader(input);
new OutputStreamWriter(output);
NumberFormat.getIntegerInstance();
}
}
"""
writeJavaSource("Example", implicit)

val failed = run("check", expectFailure = true)

assertThat(failed.task(":forbiddenApisMain")?.outcome)
.describedAs(failed.output).isEqualTo(TaskOutcome.FAILED)
assertThat(failed.output).contains(
"java.lang.String#toLowerCase()",
"java.lang.String#toUpperCase()",
"java.lang.String#format(java.lang.String,java.lang.Object[])",
"java.lang.String#getBytes()",
"java.lang.String#<init>(byte[])",
"java.lang.String#<init>(byte[],int,int)",
"java.io.InputStreamReader#<init>(java.io.InputStream)",
"java.io.OutputStreamWriter#<init>(java.io.OutputStream)",
"java.text.NumberFormat#getIntegerInstance()"
)

writeJavaSource(
"Example",
"""
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.Locale;

public class Example {
public void explicit(byte[] bytes, InputStream input, OutputStream output) throws Exception {
"IDENTIFIER".toLowerCase(Locale.ROOT);
"identifier".toUpperCase(Locale.ROOT);
String.format(Locale.ROOT, "%d", 123);
"text".getBytes(StandardCharsets.UTF_8);
new String(bytes, StandardCharsets.UTF_8);
new String(bytes, 0, bytes.length, StandardCharsets.UTF_8);
new InputStreamReader(input, StandardCharsets.UTF_8);
new OutputStreamWriter(output, StandardCharsets.UTF_8);
NumberFormat.getIntegerInstance(Locale.ROOT);
Integer.toString(123);
Long.toString(123L);
}
}
"""
)
val explicit = run("check")
assertThat(explicit.task(":forbiddenApisMain")?.outcome)
.describedAs(explicit.output).isEqualTo(TaskOutcome.SUCCESS)

writeJavaSource("Example", implicit.replace(
"public void defaults",
"// Exercise the JVM defaults deliberately.\n @SuppressForbidden\n public void defaults"
))
val suppressed = run("check")
assertThat(suppressed.task(":forbiddenApisMain")?.outcome)
.describedAs(suppressed.output).isEqualTo(TaskOutcome.SUCCESS)
}

@Test
fun `fails check on a forbidden call and allows compile-only suppression`() {
writeRootProject(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public static boolean isZulu8() {
}

public static boolean isGraalVM() {
return runtime.vendorVersion.toLowerCase().contains("graalvm");
return runtime.vendorVersion.toLowerCase(Locale.ROOT).contains("graalvm");
}

public static String getLangVersion() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import static java.util.Locale.ROOT;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -88,7 +90,10 @@ public static boolean isMusl() {
}

private static boolean isMuslProcSelfMaps() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) {
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream("/proc/self/maps"), StandardCharsets.ISO_8859_1))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("-musl-")) {
Expand Down
18 changes: 15 additions & 3 deletions components/json/src/main/java/datadog/json/JsonReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
Expand Down Expand Up @@ -490,22 +491,33 @@ private String readLiteral(int length) throws IOException {
private IOException unexpectedSyntaxError(String expected, String found) {
return new IOException(
String.format(
Locale.ROOT,
"Syntax error at line %d, position %d: expected %s but found '%s'",
this.lineNumber, this.linePosition, expected, found));
this.lineNumber,
this.linePosition,
expected,
found));
}

private IOException unexpectedSyntaxError(String expected, char found) {
return new IOException(
String.format(
Locale.ROOT,
"Syntax error at line %d, position %d: expected %s but found '%s'",
this.lineNumber, this.linePosition, expected, found));
this.lineNumber,
this.linePosition,
expected,
found));
}

private IOException syntaxError(String message) {
return new IOException(
String.format(
Locale.ROOT,
"Syntax error at line %d, position %d: %s",
this.lineNumber, this.linePosition, message));
this.lineNumber,
this.linePosition,
message));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -107,7 +108,7 @@ public static void install() {
}
String endpoint = config.getAiGuardEndpoint();
if (isBlank(endpoint)) {
endpoint = String.format("https://app.%s/api/v2/ai-guard", config.getSite());
endpoint = String.format(Locale.ROOT, "https://app.%s/api/v2/ai-guard", config.getSite());
}
final Map<String, String> headers =
mapOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -51,14 +52,18 @@ public class InjectingPipeOutputStreamBenchmark {
public void withPipe() throws Exception {
try (final PrintWriter out =
new PrintWriter(
new InjectingPipeOutputStream(new ByteArrayOutputStream(), marker, content))) {
new OutputStreamWriter(
new InjectingPipeOutputStream(new ByteArrayOutputStream(), marker, content),
StandardCharsets.UTF_8))) {
htmlContent.forEach(out::println);
}
}

@Benchmark
public void withoutPipe() throws Exception {
try (final PrintWriter out = new PrintWriter(new ByteArrayOutputStream())) {
try (final PrintWriter out =
new PrintWriter(
new OutputStreamWriter(new ByteArrayOutputStream(), StandardCharsets.UTF_8))) {
htmlContent.forEach(out::println);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.slf4j.Logger;
Expand All @@ -30,7 +31,10 @@ public class StaticEventLogger {
String fileName = dir + "startup_" + System.currentTimeMillis() + ".csv";

try {
writer = new BufferedWriter(new FileWriter(fileName), 32768);
writer =
new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(fileName), StandardCharsets.UTF_8),
32768);
writer.write("Event,State,Timestamp");
writer.newLine();
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import datadog.trace.api.git.PersonInfo;
import datadog.trace.civisibility.ci.env.CiEnvironment;
import datadog.trace.util.Strings;
import java.util.Locale;
import javax.annotation.Nonnull;

class AppVeyorInfo implements CIProviderInfo {
Expand Down Expand Up @@ -65,7 +66,7 @@ private static String getCommitMessage(String messageSubject, String messageBody
if (messageBody == null) {
return messageSubject;
}
return String.format("%s%n%s", messageSubject, messageBody);
return String.format(Locale.ROOT, "%s%n%s", messageSubject, messageBody);
}

@Override
Expand Down Expand Up @@ -126,13 +127,14 @@ private String buildGitCommit() {

private String buildGitRepositoryUrl(final String repoProvider, final String repoName) {
if ("github".equals(repoProvider) && (repoName != null && !repoName.isEmpty())) {
return String.format("https://github.com/%s.git", repoName);
return String.format(Locale.ROOT, "https://github.com/%s.git", repoName);
}
return null;
}

private String buildPipelineUrl(final String repoName, final String buildId) {
return String.format("https://ci.appveyor.com/project/%s/builds/%s", repoName, buildId);
return String.format(
Locale.ROOT, "https://ci.appveyor.com/project/%s/builds/%s", repoName, buildId);
}

private PersonInfo buildGitCommitAuthor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import datadog.trace.api.git.GitInfo;
import datadog.trace.api.git.PersonInfo;
import datadog.trace.civisibility.ci.env.CiEnvironment;
import java.util.Locale;
import javax.annotation.Nonnull;

class AzurePipelinesInfo implements CIProviderInfo {
Expand Down Expand Up @@ -145,11 +146,17 @@ private String buildCiJobUrl(
final String jobId,
final String taskId) {
return String.format(
"%s%s/_build/results?buildId=%s&view=logs&j=%s&t=%s", uri, project, buildId, jobId, taskId);
Locale.ROOT,
"%s%s/_build/results?buildId=%s&view=logs&j=%s&t=%s",
uri,
project,
buildId,
jobId,
taskId);
}

private String buildCiPipelineUrl(final String uri, final String project, final String buildId) {
return String.format("%s%s/_build/results?buildId=%s", uri, project, buildId);
return String.format(Locale.ROOT, "%s%s/_build/results?buildId=%s", uri, project, buildId);
}

private PersonInfo buildGitCommitAuthor() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import datadog.trace.api.git.GitInfo;
import datadog.trace.civisibility.ci.env.CiEnvironment;
import datadog.trace.util.Strings;
import java.util.Locale;
import javax.annotation.Nonnull;

class BitBucketInfo implements CIProviderInfo {
Expand Down Expand Up @@ -86,7 +87,7 @@ public PullRequestInfo buildPullRequestInfo() {

private String buildPipelineUrl(final String repo, final String number) {
return String.format(
"https://bitbucket.org/%s/addon/pipelines/home#!/results/%s", repo, number);
Locale.ROOT, "https://bitbucket.org/%s/addon/pipelines/home#!/results/%s", repo, number);
}

private String buildPipelineId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import datadog.trace.api.git.GitInfo;
import datadog.trace.api.git.PersonInfo;
import datadog.trace.civisibility.ci.env.CiEnvironment;
import java.util.Locale;
import javax.annotation.Nonnull;

class BuddyInfo implements CIProviderInfo {
Expand Down Expand Up @@ -80,7 +81,7 @@ private String getPipelineId(String pipelineNumber) {
if (pipelineNumber == null) {
return pipelineId;
}
return String.format("%s/%s", pipelineId, pipelineNumber);
return String.format(Locale.ROOT, "%s/%s", pipelineId, pipelineNumber);
}

private PersonInfo buildGitCommiter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ public CIInfo buildCIInfo() {
.ciPipelineNumber(environment.get(BUILDKITE_PIPELINE_NUMBER))
.ciPipelineUrl(ciPipelineUrl)
.ciJobId(environment.get(BUILDKITE_JOB_ID))
.ciJobUrl(String.format("%s#%s", ciPipelineUrl, environment.get(BUILDKITE_JOB_ID)))
.ciJobUrl(
String.format(Locale.ROOT, "%s#%s", ciPipelineUrl, environment.get(BUILDKITE_JOB_ID)))
.ciWorkspace(expandTilde(environment.get(BUILDKITE_WORKSPACE_PATH)))
.ciNodeName(environment.get(BUILDKITE_AGENT_ID))
.ciNodeLabels(buildCiNodeLabels())
Expand Down
Loading
Loading