From 5ddf4d399667a04790191c7041a62601dee878d2 Mon Sep 17 00:00:00 2001 From: Shannon Pamperl Date: Tue, 14 Nov 2023 09:40:50 -0600 Subject: [PATCH] Add FindFeatureFlag recipe --- build.gradle.kts | 1 + .../launchdarkly/search/FindFeatureFlag.java | 167 +++++++++ .../search/FindFeatureFlagTest.java | 319 ++++++++++++++++++ 3 files changed, 487 insertions(+) create mode 100644 src/main/java/org/openrewrite/launchdarkly/search/FindFeatureFlag.java create mode 100644 src/test/java/org/openrewrite/launchdarkly/search/FindFeatureFlagTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 7e8e306..f2f3363 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,6 +9,7 @@ val rewriteVersion = rewriteRecipe.rewriteVersion.get() dependencies { implementation(platform("org.openrewrite:rewrite-bom:$rewriteVersion")) implementation("org.openrewrite:rewrite-java") + implementation("org.openrewrite.meta:rewrite-analysis:$rewriteVersion") implementation("org.openrewrite.recipe:rewrite-java-dependencies:$rewriteVersion") testImplementation("org.openrewrite:rewrite-java-17") diff --git a/src/main/java/org/openrewrite/launchdarkly/search/FindFeatureFlag.java b/src/main/java/org/openrewrite/launchdarkly/search/FindFeatureFlag.java new file mode 100644 index 0000000..3cd61e3 --- /dev/null +++ b/src/main/java/org/openrewrite/launchdarkly/search/FindFeatureFlag.java @@ -0,0 +1,167 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.launchdarkly.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.analysis.InvocationMatcher; +import org.openrewrite.analysis.dataflow.DataFlowNode; +import org.openrewrite.analysis.dataflow.DataFlowSpec; +import org.openrewrite.analysis.dataflow.Dataflow; +import org.openrewrite.analysis.trait.expr.Expr; +import org.openrewrite.analysis.trait.expr.Literal; +import org.openrewrite.analysis.trait.expr.VarAccess; +import org.openrewrite.analysis.trait.variable.Variable; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; + +@Value +@EqualsAndHashCode(callSuper = true) +public class FindFeatureFlag extends Recipe { + @Option(displayName = "Flag Type", + description = "The feature flag's type.", + example = "Bool", + valid = {"Bool", "Double", "Int", "JsonValue", "String"}) + @Nullable + FeatureFlagType flagType; + + @Option(displayName = "Feature Key", + description = "The unique key for the feature flag.", + example = "flag-key-123abc") + @Nullable + String featureKey; + + @Override + public String getDisplayName() { + return "Find a LaunchDarkly feature flag"; + } + + @Override + public String getDescription() { + return "Find a LaunchDarkly feature flag."; + } + + @Override + public TreeVisitor getVisitor() { + MethodMatcher launchDarklyClientMatcher = new MethodMatcher("com.launchdarkly.sdk.server.LDClient *Variation(..)"); + return new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation m = super.visitMethodInvocation(method, ctx); + if (!launchDarklyClientMatcher.matches(m)) { + return m; + } + + if (flagType != null && featureKey != null) { + MethodMatcher flagTypeMatcher = flagType.asMethodMatcher(); + Boolean matchesFeatureKey = getCursor().getMessage("feature.found"); + if (flagTypeMatcher.matches(m) && matchesFeatureKey != null && matchesFeatureKey) { + return SearchResult.found(m); + } + } else if (flagType != null) { + MethodMatcher flagTypeMatcher = flagType.asMethodMatcher(); + if (flagTypeMatcher.matches(m)) { + return SearchResult.found(m); + } + } else if (featureKey != null) { + Boolean matchesFeatureKey = getCursor().getMessage("feature.found"); + if (matchesFeatureKey != null && matchesFeatureKey) { + return SearchResult.found(m); + } + } else { + return SearchResult.found(m); + } + + return m; + } + + @Override + public Expression visitExpression(Expression expression, ExecutionContext ctx) { + Expression e = super.visitExpression(expression, ctx); + if (StringUtils.isBlank(featureKey)) { + return e; + } + + InvocationMatcher matcher = InvocationMatcher.fromMethodMatcher(launchDarklyClientMatcher); + boolean found = Dataflow.startingAt(getCursor()) + .findSinks(new DataFlowSpec() { + @Override + public boolean isSource(DataFlowNode srcNode) { + return srcNode.asExpr(VarAccess.class) + .map(VarAccess::getVariable) + .map(Variable::getAssignedValues) + .bind(assignedVariables -> { + if (assignedVariables.size() > 1) { + return fj.data.Option.none(); + } + for (Expr e : assignedVariables) { + if (e instanceof Literal) { + Literal l = (Literal) e; + return l.getValue() + .map(featureKey::equals); + } + } + return fj.data.Option.none(); + }).orElse(() -> srcNode + .asExpr(Literal.class) + .bind(Literal::getValue) + .map(featureKey::equals)) + .orSome(false); + } + + @Override + public boolean isSink(DataFlowNode sinkNode) { + return matcher.advanced().isFirstParameter(sinkNode.getCursor()); + } + }).isSome(); + if (found) { + J.MethodInvocation m = getCursor().firstEnclosing(J.MethodInvocation.class); + if (launchDarklyClientMatcher.matches(m)) { + getCursor().putMessageOnFirstEnclosing(J.MethodInvocation.class, "feature.found", true); + } + } + return e; + } + }; + } + + public enum FeatureFlagType { + Bool("boolVariation"), + Double("doubleVariation"), + Int("intVariation"), + JsonValue("jsonValueVariation"), + String("stringVariation"); + + String methodName; + + FeatureFlagType(String methodName) { + this.methodName = methodName; + } + + public MethodMatcher asMethodMatcher() { + return new MethodMatcher("com.launchdarkly.sdk.server.LDClient " + methodName + "(..)"); + } + } +} diff --git a/src/test/java/org/openrewrite/launchdarkly/search/FindFeatureFlagTest.java b/src/test/java/org/openrewrite/launchdarkly/search/FindFeatureFlagTest.java new file mode 100644 index 0000000..5fb0ad8 --- /dev/null +++ b/src/test/java/org/openrewrite/launchdarkly/search/FindFeatureFlagTest.java @@ -0,0 +1,319 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://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 org.openrewrite.launchdarkly.search; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class FindFeatureFlagTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.parser(JavaParser.fromJavaVersion().classpath("launchdarkly-java-server-sdk")); + } + + @Test + @DocumentExample + void findFeatureFlag() { + rewriteRun( + spec -> spec.recipe(new FindFeatureFlag(null, null)), + java( + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """, + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = /*~~>*/client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """ + ) + ); + } + + @Test + void findFeatureFlagByType() { + rewriteRun( + spec -> spec.recipe(new FindFeatureFlag(FindFeatureFlag.FeatureFlagType.Bool, null)), + java( + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + String flagValue2 = client.stringVariation("flag-key-789def", user, "on"); + if ("on".equals(flagValue2)) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """, + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = /*~~>*/client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + String flagValue2 = client.stringVariation("flag-key-789def", user, "on"); + if ("on".equals(flagValue2)) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """ + ) + ); + } + + @Test + void findFeatureFlagByName() { + rewriteRun( + spec -> spec.recipe(new FindFeatureFlag(null, "flag-key-123abc")), + java( + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + boolean flagValue2 = client.boolVariation("flag-key-789def", user, false); + if (flagValue2) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """, + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = /*~~>*/client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + boolean flagValue2 = client.boolVariation("flag-key-789def", user, false); + if (flagValue2) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """ + ) + ); + } + + @Test + void findFeatureFlagByTypeAndName() { + rewriteRun( + spec -> spec.recipe(new FindFeatureFlag(FindFeatureFlag.FeatureFlagType.Bool, "flag-key-123abc")), + java( + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + String flagValue2 = client.stringVariation("flag-key-123abc", user, "on"); + if ("on".equals(flagValue2)) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """, + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = /*~~>*/client.boolVariation("flag-key-123abc", user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + String flagValue2 = client.stringVariation("flag-key-123abc", user, "on"); + if ("on".equals(flagValue2)) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """ + ) + ); + } + + @Test + void findFlagByNameUsingVariable() { + rewriteRun( + spec -> spec.recipe(new FindFeatureFlag(null, "flag-key-123abc")), + java( + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + private static final String FEATURE_FLAG = "flag-key-123abc"; + private static final String FEATURE2_FLAG = "flag-key-789def"; + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = client.boolVariation(FEATURE_FLAG, user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + boolean flagValue2 = client.boolVariation(FEATURE2_FLAG, user, false); + if (flagValue2) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """, + """ + import com.launchdarkly.sdk.LDUser; + import com.launchdarkly.sdk.server.LDClient; + + class Test { + private static final String FEATURE_FLAG = "flag-key-123abc"; + private static final String FEATURE2_FLAG = "flag-key-789def"; + public void a() { + LDClient client = new LDClient("sdk-key"); + LDUser user = new LDUser.Builder("user-key") + .name("user") + .build(); + boolean flagValue = /*~~>*/client.boolVariation(FEATURE_FLAG, user, false); + if (flagValue) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + boolean flagValue2 = client.boolVariation(FEATURE2_FLAG, user, false); + if (flagValue2) { + // Application code to show the feature + } else { + // The code to run if the feature is off + } + } + } + """ + ) + ); + } +}