Skip to content

Commit

Permalink
Gauges should be registered with registerWithReplacement (#1123)
Browse files Browse the repository at this point in the history
Using TaggedMetricRegistry.gauge is equivalent to map.putIfAbsent, and
can result in subtle resource leaks. Prefer replacing existing gauge
values using `registerWithReplacement`.

This check doesn't apply unless a new enough version of Tritium
is available on the compilation classpath.
  • Loading branch information
carterkozak authored and bulldozer-bot[bot] committed Dec 17, 2019
1 parent 020ebae commit af50c26
Show file tree
Hide file tree
Showing 16 changed files with 362 additions and 17 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Safe Logging can be found at [github.com/palantir/safe-logging](https://github.c
- `AssertjPrimitiveComparison`: Prefer using AssertJ fluent comparisons over logic in an assertThat statement.
- `ExceptionSpecificity`: Prefer more specific catch types than Exception and Throwable.
- `ThrowSpecificity`: Prefer to declare more specific `throws` types than Exception and Throwable.
- `UnsafeGaugeRegistration`: Use TaggedMetricRegistry.registerWithReplacement over TaggedMetricRegistry.gauge.

### Programmatic Application

Expand Down
1 change: 1 addition & 0 deletions baseline-error-prone/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
testCompile 'commons-lang:commons-lang'
testCompile 'org.assertj:assertj-core'
testCompile 'org.jooq:jooq'
testCompile 'com.palantir.tritium:tritium-registry'
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-migrationsupport'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.AbstractReturnValueIgnored;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.method.MethodMatchers;
Expand Down Expand Up @@ -60,7 +59,7 @@ public Description matchMethodInvocation(MethodInvocationTree methodInvocationTr
return description;
}
return buildDescription(methodInvocationTree)
.addFix(SuggestedFixes.renameMethodInvocation(methodInvocationTree, "execute", state))
.addFix(MoreSuggestedFixes.renameMethodInvocation(methodInvocationTree, "execute", state))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@

package com.palantir.baseline.errorprone;

import com.google.common.collect.Lists;
import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.util.ErrorProneToken;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.parser.Tokens;
import com.sun.tools.javac.tree.JCTree;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.lang.model.element.Name;

/**
* Additional utility functionality for {@link SuggestedFix} objects.
Expand Down Expand Up @@ -67,6 +72,41 @@ static SuggestedFix renameInvocationRetainingTypeArguments(
return fix.replace(startPos, endPos, extra + newMethodName).build();
}

/**
* Replaces the name of the method being invoked in {@code tree} with {@code replacement}.
* This implementation is forked from upstream error-prone to work around a bug that results in
* parameters being renamed in some scenarios: https://github.com/google/error-prone/issues/1451.
*/
public static SuggestedFix renameMethodInvocation(
MethodInvocationTree tree,
String replacement,
VisitorState state) {
Tree methodSelect = tree.getMethodSelect();
Name identifier;
int startPos;
if (methodSelect instanceof MemberSelectTree) {
identifier = ((MemberSelectTree) methodSelect).getIdentifier();
startPos = state.getEndPosition(((MemberSelectTree) methodSelect).getExpression());
} else if (methodSelect instanceof IdentifierTree) {
identifier = ((IdentifierTree) methodSelect).getName();
startPos = ((JCTree) tree).getStartPosition();
} else {
throw new IllegalStateException("Malformed tree:\n" + state.getSourceForNode(tree));
}
List<ErrorProneToken> tokens = state.getOffsetTokens(startPos, state.getEndPosition(tree));
int depth = 0;
for (ErrorProneToken token : Lists.reverse(tokens)) {
if (depth == 0 && token.kind() == Tokens.TokenKind.IDENTIFIER && token.name().equals(identifier)) {
return SuggestedFix.replace(token.pos(), token.endPos(), replacement);
} else if (token.kind() == Tokens.TokenKind.RPAREN) {
depth++;
} else if (token.kind() == Tokens.TokenKind.LPAREN) {
depth--;
}
}
throw new IllegalStateException("Malformed tree:\n" + state.getSourceForNode(tree));
}

/**
* Identical to {@link SuggestedFixes#qualifyType(VisitorState, SuggestedFix.Builder, String)} unless the
* compiling JVM is not supported by error-prone (JDK13) in which case a fallback is attempted.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import com.google.errorprone.bugpatterns.AbstractReturnValueIgnored;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
Expand Down Expand Up @@ -121,7 +120,7 @@ public Description describeReturnValueIgnored(MethodInvocationTree methodInvocat
}
if (RAF_BUFFER_READ_MATCHER.matches(methodInvocationTree, state)) {
return buildDescription(methodInvocationTree)
.addFix(SuggestedFixes.renameMethodInvocation(methodInvocationTree, "readFully", state))
.addFix(MoreSuggestedFixes.renameMethodInvocation(methodInvocationTree, "readFully", state))
.build();
}
if (READER_SKIP_MATCHER.matches(methodInvocationTree, state)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.method.MethodMatchers;
Expand Down Expand Up @@ -56,15 +55,15 @@ public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState
// Suggested fix exists to provide context when compilation fails, it shouldn't be used
// as a drop in replacement because the unresolved string may not be sufficient in some
// cases, particularly involving auditing.
.addFix(SuggestedFixes.renameMethodInvocation(tree, "getHostString", state))
.addFix(MoreSuggestedFixes.renameMethodInvocation(tree, "getHostString", state))
.build();
}
if (INET_ADDRESS_MATCHER.matches(tree, state)) {
return buildDescription(tree)
// Suggested fix exists to provide context when compilation fails, it shouldn't be used
// as a drop in replacement because the unresolved string may not be sufficient in some
// cases, particularly involving auditing.
.addFix(SuggestedFixes.renameMethodInvocation(tree, "getHostAddress", state))
.addFix(MoreSuggestedFixes.renameMethodInvocation(tree, "getHostAddress", state))
.build();
}
return Description.NO_MATCH;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.IfTreeMatcher;
import com.google.errorprone.fixes.SuggestedFixes;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.method.MethodMatchers;
Expand Down Expand Up @@ -86,7 +85,7 @@ public Description matchIf(IfTree tree, VisitorState state) {
return Description.NO_MATCH;
}
return buildDescription(tree)
.addFix(SuggestedFixes.renameMethodInvocation(
.addFix(MoreSuggestedFixes.renameMethodInvocation(
levelCheckInvocation, mostSevere.levelCheckMethodName(), state))
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* (c) Copyright 2018 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.baseline.errorprone;

import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.AbstractReturnValueIgnored;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.method.MethodMatchers;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Symbol;

@AutoService(BugChecker.class)
@BugPattern(
name = "UnsafeGaugeRegistration",
link = "https://github.com/palantir/gradle-baseline#baseline-error-prone-checks",
linkType = BugPattern.LinkType.CUSTOM,
providesFix = BugPattern.ProvidesFix.REQUIRES_HUMAN_ATTENTION,
severity = BugPattern.SeverityLevel.WARNING,
summary = "Using TaggedMetricRegistry.gauge is equivalent to map.putIfAbsent, and can result in subtle "
+ "resource leaks. Prefer replacing existing gauge values.\n"
// This check may begin to fail after a version upgrade, where fixes aren't automatically applied
+ "This can be fixed automatically using "
+ "./gradlew compileJava compileTestJava -PerrorProneApply=UnsafeGaugeRegistration")
public final class UnsafeGaugeRegistration extends AbstractReturnValueIgnored {

private static final String TAGGED_REGISTRY = "com.palantir.tritium.metrics.registry.TaggedMetricRegistry";
private static final String REGISTER_WITH_REPLACEMENT = "registerWithReplacement";
private static final Matcher<ExpressionTree> MATCHER = MethodMatchers.instanceMethod()
.onDescendantOf(TAGGED_REGISTRY)
.named("gauge")
.withParameters("com.palantir.tritium.metrics.registry.MetricName", "com.codahale.metrics.Gauge");

@Override
public Matcher<? super ExpressionTree> specializedMatcher() {
return MATCHER;
}

// Override matchMethodInvocation from AbstractReturnValueIgnored to apply our suggested fix.
@Override
public Description matchMethodInvocation(MethodInvocationTree methodInvocationTree, VisitorState state) {
Description description = super.matchMethodInvocation(methodInvocationTree, state);
if (Description.NO_MATCH.equals(description)
|| !hasRegisterWithReplacement(state)
|| TestCheckUtils.isTestCode(state)) {
return Description.NO_MATCH;
}
return buildDescription(methodInvocationTree)
.addFix(MoreSuggestedFixes.renameMethodInvocation(
methodInvocationTree, REGISTER_WITH_REPLACEMENT, state))
.build();
}

/**
* TaggedMetricRegistry.registerWithReplacement was added in Tritium 0.16.1, avoid flagging older versions.
*/
private static boolean hasRegisterWithReplacement(VisitorState state) {
Symbol symbol = state.getSymbolFromString(TAGGED_REGISTRY);
if (!(symbol instanceof Symbol.ClassSymbol)) {
return false;
}
Symbol.ClassSymbol classSymbol = (Symbol.ClassSymbol) symbol;
for (Symbol enclosed : classSymbol.getEnclosedElements()) {
if (enclosed instanceof Symbol.MethodSymbol) {
Symbol.MethodSymbol enclosedMethod = (Symbol.MethodSymbol) enclosed;
if (enclosedMethod.name.contentEquals(REGISTER_WITH_REPLACEMENT)) {
return true;
}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* (c) Copyright 2018 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.baseline.errorprone;

import com.google.errorprone.BugCheckerRefactoringTestHelper;
import com.google.errorprone.CompilationTestHelper;
import org.junit.jupiter.api.Test;

class UnsafeGaugeRegistrationTest {

@Test
void testFix() {
RefactoringValidator.of(new UnsafeGaugeRegistration(), getClass())
.addInputLines(
"Test.java",
"import com.palantir.tritium.metrics.registry.MetricName;",
"import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;",
"class Test {",
" void f(TaggedMetricRegistry registry, MetricName name) {",
" registry.gauge(name, () -> 1);",
" }",
"}")
.addOutputLines(
"Test.java",
"import com.palantir.tritium.metrics.registry.MetricName;",
"import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;",
"class Test {",
" void f(TaggedMetricRegistry registry, MetricName name) {",
" registry.registerWithReplacement(name, () -> 1);",
" }",
"}"
)
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
}

@Test
void testKnownBug() {
RefactoringValidator.of(new UnsafeGaugeRegistration(), getClass())
.addInputLines(
"Test.java",
"import com.codahale.metrics.Gauge;",
"import com.palantir.tritium.metrics.registry.MetricName;",
"import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;",
"class Test {",
" void f(TaggedMetricRegistry registry, MetricName name, Gauge<?> gauge) {",
" registry.gauge(name, gauge);",
" }",
"}")
.addOutputLines(
"Test.java",
"import com.codahale.metrics.Gauge;",
"import com.palantir.tritium.metrics.registry.MetricName;",
"import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;",
"class Test {",
" void f(TaggedMetricRegistry registry, MetricName name, Gauge<?> gauge) {",
// Tests our workaround for https://github.com/google/error-prone/issues/1451
" registry.registerWithReplacement(name, gauge);",
" }",
"}"
)
.doTest(BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH);
}

@Test
void testNegative() {
CompilationTestHelper.newInstance(UnsafeGaugeRegistration.class, getClass()).addSourceLines(
"Test.java",
"import com.codahale.metrics.Gauge;",
"import com.palantir.tritium.metrics.registry.MetricName;",
"import com.palantir.tritium.metrics.registry.TaggedMetricRegistry;",
"class Test {",
" Gauge<?> f(TaggedMetricRegistry registry, MetricName name) {",
" return registry.gauge(name, () -> 1);",
" }",
"}").doTest();
}
}
4 changes: 4 additions & 0 deletions baseline-refaster-rules/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ dependencies {
implementation 'com.google.errorprone:error_prone_refaster'
implementation 'org.assertj:assertj-core'
implementation 'org.mockito:mockito-core'
implementation 'com.palantir.tritium:tritium-registry'

testCompile 'junit:junit'
testCompile project(':baseline-refaster-testing')
testCompile 'org.immutables:value::annotations'

compileOnly 'org.immutables:value::annotations'
}

// Do not apply refaster rules
Expand Down
Loading

0 comments on commit af50c26

Please sign in to comment.