-
Notifications
You must be signed in to change notification settings - Fork 365
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Autodetect formatting style of JSON code (#4916)
* feat: recipe for adding a key value pair to a json * Slight polish * Slight polish * Strive for better formatting after insertion * Improve some existing cases already * Update test as suggested * Refactoring, extract normalizeNewLines() * Autoformat visitor for JSON * Removing @NotNull annotations * Basic tests for Autodetect * Basic tests for NormalizeLineBreaksVisitor * No public classifier Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Copy&paste typo Co-authored-by: Knut Wannheden <[email protected]> * Copy&paste typo Co-authored-by: Knut Wannheden <[email protected]> * Rename FindLineFormatJsonVisitor * Adding package-info.java files * Parsing the value parameter to JSON * Fixed description * Removing unneeded unQuote method --------- Co-authored-by: dpozinen <[email protected]> Co-authored-by: Tim te Beek <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Knut Wannheden <[email protected]>
- Loading branch information
1 parent
1d96858
commit afa160e
Showing
15 changed files
with
1,199 additions
and
6 deletions.
There are no files selected for viewing
135 changes: 135 additions & 0 deletions
135
rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.json; | ||
|
||
import lombok.EqualsAndHashCode; | ||
import lombok.Value; | ||
import org.intellij.lang.annotations.Language; | ||
import org.openrewrite.ExecutionContext; | ||
import org.openrewrite.Option; | ||
import org.openrewrite.Recipe; | ||
import org.openrewrite.TreeVisitor; | ||
import org.openrewrite.internal.ListUtils; | ||
import org.openrewrite.json.tree.*; | ||
import org.openrewrite.marker.Markers; | ||
|
||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
import static java.util.Collections.emptyList; | ||
import static org.openrewrite.Tree.randomId; | ||
|
||
@Value | ||
@EqualsAndHashCode(callSuper = false) | ||
public class AddKeyValue extends Recipe { | ||
|
||
@Option(displayName = "Key path", | ||
description = "A JsonPath expression to locate the *parent* JSON entry.", | ||
example = "'$.subjects.*' or '$.' or '$.x[1].y.*' etc.") | ||
String keyPath; | ||
|
||
@Option(displayName = "Key", | ||
description = "The key to create.", | ||
example = "myKey") | ||
String key; | ||
|
||
@Option(displayName = "Value", | ||
description = "The value to add to the document at the specified key. Can be of any type representing JSON value." + | ||
" String values should be quoted to be inserted as Strings.", | ||
example = "`\"myValue\"` or `{\"a\": 1}` or `[ 123 ]`") | ||
@Language("Json") | ||
String value; | ||
|
||
@Option(displayName = "Prepend", | ||
required = false, | ||
description = "If set to `true` the value will be added to the beginning of the object") | ||
boolean prepend; | ||
|
||
@Override | ||
public String getDisplayName() { | ||
return "Add value to JSON Object"; | ||
} | ||
|
||
@Override | ||
public String getDescription() { | ||
return "Adds a `value` at the specified `keyPath` with the specified `key`, if the key doesn't already exist."; | ||
} | ||
|
||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getVisitor() { | ||
return new JsonIsoVisitor<ExecutionContext>() { | ||
private final JsonPathMatcher pathMatcher = new JsonPathMatcher(keyPath); | ||
|
||
@Override | ||
public Json.JsonObject visitObject(Json.JsonObject obj, ExecutionContext ctx) { | ||
obj = super.visitObject(obj, ctx); | ||
|
||
if (pathMatcher.matches(getCursor()) && objectDoesNotContainKey(obj, key)) { | ||
List<Json> originalMembers = obj.getMembers(); | ||
boolean jsonIsEmpty = originalMembers.isEmpty() || originalMembers.get(0) instanceof Json.Empty; | ||
Space space = jsonIsEmpty || prepend ? originalMembers.get(0).getPrefix() : Space.build("\n", emptyList()); | ||
|
||
Json newMember = new Json.Member(randomId(), space, Markers.EMPTY, rightPaddedKey(), parsedValue()); | ||
|
||
if (jsonIsEmpty) { | ||
return autoFormat(obj.withMembers(Collections.singletonList(newMember)), ctx, getCursor().getParent()); | ||
} | ||
|
||
List<Json> newMembers = prepend ? | ||
ListUtils.concat(newMember, originalMembers) : | ||
ListUtils.concat(originalMembers, newMember); | ||
return autoFormat(obj.withMembers(newMembers), ctx, getCursor().getParent()); | ||
} | ||
return obj; | ||
} | ||
|
||
private JsonValue parsedValue() { | ||
Json.Document parsedDoc = (Json.Document) JsonParser.builder().build() | ||
.parse(value.trim()).findFirst().get(); | ||
JsonValue value = parsedDoc.getValue(); | ||
return value.withPrefix(value.getPrefix().withWhitespace(" ")); | ||
} | ||
|
||
private JsonRightPadded<JsonKey> rightPaddedKey() { | ||
return new JsonRightPadded<>( | ||
new Json.Literal(randomId(), Space.EMPTY, Markers.EMPTY, "\"" + key + "\"", key), | ||
Space.EMPTY, Markers.EMPTY | ||
); | ||
} | ||
|
||
private boolean objectDoesNotContainKey(Json.JsonObject obj, String key) { | ||
for (Json member : obj.getMembers()) { | ||
if (member instanceof Json.Member) { | ||
if (keyMatches(((Json.Member) member).getKey(), key)) { | ||
return false; | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
private boolean keyMatches(JsonKey jsonKey, String key) { | ||
if (jsonKey instanceof Json.Literal) { | ||
return key.equals(((Json.Literal) jsonKey).getValue()); | ||
} else if (jsonKey instanceof Json.Identifier) { | ||
return key.equals(((Json.Identifier) jsonKey).getName()); | ||
} | ||
return false; | ||
} | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
rewrite-json/src/main/java/org/openrewrite/json/format/AutoFormatVisitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* Copyright 2025 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.json.format; | ||
|
||
import org.jspecify.annotations.Nullable; | ||
import org.openrewrite.Cursor; | ||
import org.openrewrite.Tree; | ||
import org.openrewrite.json.JsonIsoVisitor; | ||
import org.openrewrite.json.style.Autodetect; | ||
import org.openrewrite.json.style.TabsAndIndentsStyle; | ||
import org.openrewrite.json.tree.Json; | ||
import org.openrewrite.style.GeneralFormatStyle; | ||
import org.openrewrite.style.NamedStyles; | ||
|
||
import java.util.Optional; | ||
|
||
import static java.util.Collections.singletonList; | ||
|
||
public class AutoFormatVisitor<P> extends JsonIsoVisitor<P> { | ||
@Nullable | ||
private final Tree stopAfter; | ||
|
||
public AutoFormatVisitor(@Nullable Tree stopAfter) { | ||
this.stopAfter = stopAfter; | ||
} | ||
|
||
@Override | ||
public Json preVisit(Json tree, P p) { | ||
stopAfterPreVisit(); | ||
Json.Document doc = getCursor().firstEnclosingOrThrow(Json.Document.class); | ||
Cursor cursor = getCursor().getParentOrThrow(); | ||
Autodetect autodetectedStyle = Autodetect.detector().sample(doc).build(); | ||
Json js = tree; | ||
|
||
TabsAndIndentsStyle taiStyle = Optional.ofNullable(doc.getStyle(TabsAndIndentsStyle.class)) | ||
.orElseGet(() -> NamedStyles.merge(TabsAndIndentsStyle.class, singletonList(autodetectedStyle))); | ||
assert(taiStyle != null); | ||
js = new TabsAndIndentsVisitor<>(taiStyle, stopAfter).visitNonNull(js, p, cursor.fork()); | ||
|
||
GeneralFormatStyle gfStyle = Optional.ofNullable(doc.getStyle(GeneralFormatStyle.class)) | ||
.orElseGet(() -> NamedStyles.merge(GeneralFormatStyle.class, singletonList(autodetectedStyle))); | ||
assert(gfStyle != null); | ||
js = new NormalizeLineBreaksVisitor<>(gfStyle, stopAfter).visitNonNull(js, p, cursor.fork()); | ||
|
||
return js; | ||
} | ||
|
||
@Override | ||
public Json.Document visitDocument(Json.Document js, P p) { | ||
Autodetect autodetectedStyle = Autodetect.detector().sample(js).build(); | ||
|
||
TabsAndIndentsStyle taiStyle = Optional.ofNullable(js.getStyle(TabsAndIndentsStyle.class)) | ||
.orElseGet(() -> NamedStyles.merge(TabsAndIndentsStyle.class, singletonList(autodetectedStyle))); | ||
assert(taiStyle != null); | ||
js = (Json.Document) new TabsAndIndentsVisitor<>(taiStyle, stopAfter).visitNonNull(js, p); | ||
|
||
GeneralFormatStyle gfStyle = Optional.ofNullable(js.getStyle(GeneralFormatStyle.class)) | ||
.orElseGet(() -> NamedStyles.merge(GeneralFormatStyle.class, singletonList(autodetectedStyle))); | ||
assert(gfStyle != null); | ||
js = (Json.Document) new NormalizeLineBreaksVisitor<>(gfStyle, stopAfter).visitNonNull(js, p); | ||
|
||
return js; | ||
} | ||
|
||
@Override | ||
public @Nullable Json postVisit(Json tree, P p) { | ||
if (stopAfter != null && stopAfter.isScope(tree)) { | ||
getCursor().putMessageOnFirstEnclosing(Json.Document.class, "stop", true); | ||
} | ||
return super.postVisit(tree, p); | ||
} | ||
|
||
@Override | ||
public @Nullable Json visit(@Nullable Tree tree, P p) { | ||
if (getCursor().getNearestMessage("stop") != null) { | ||
return (Json) tree; | ||
} | ||
return super.visit(tree, p); | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
rewrite-json/src/main/java/org/openrewrite/json/format/Indents.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright 2025 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.json.format; | ||
|
||
import org.openrewrite.ExecutionContext; | ||
import org.openrewrite.Recipe; | ||
import org.openrewrite.TreeVisitor; | ||
import org.openrewrite.json.JsonIsoVisitor; | ||
import org.openrewrite.json.style.Autodetect; | ||
import org.openrewrite.json.style.TabsAndIndentsStyle; | ||
import org.openrewrite.json.tree.Json; | ||
import org.openrewrite.style.NamedStyles; | ||
|
||
import static java.util.Collections.singletonList; | ||
|
||
public class Indents extends Recipe { | ||
@Override | ||
public String getDisplayName() { | ||
return "JSON indent"; | ||
} | ||
|
||
@Override | ||
public String getDescription() { | ||
return "Format tabs and indents in JSON."; | ||
} | ||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getVisitor() { | ||
return new TabsAndIndentsFromCompilationUnitStyle(); | ||
} | ||
|
||
private static class TabsAndIndentsFromCompilationUnitStyle extends JsonIsoVisitor<ExecutionContext> { | ||
@Override | ||
public Json. Document visitDocument(Json.Document docs, ExecutionContext ctx) { | ||
TabsAndIndentsStyle style = docs.getStyle(TabsAndIndentsStyle.class); | ||
if (style == null) { | ||
style = NamedStyles.merge(TabsAndIndentsStyle.class, singletonList(Autodetect.detector().sample(docs).build())); | ||
assert(style != null); | ||
} | ||
doAfterVisit(new TabsAndIndentsVisitor<>(style, null)); | ||
return docs; | ||
} | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
rewrite-json/src/main/java/org/openrewrite/json/format/NormalizeLineBreaksVisitor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* Copyright 2025 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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.json.format; | ||
|
||
import org.jspecify.annotations.Nullable; | ||
import org.openrewrite.Tree; | ||
import org.openrewrite.style.GeneralFormatStyle; | ||
import org.openrewrite.json.JsonIsoVisitor; | ||
import org.openrewrite.json.tree.Json; | ||
|
||
import static org.openrewrite.format.LineBreaks.normalizeNewLines; | ||
|
||
public class NormalizeLineBreaksVisitor<P> extends JsonIsoVisitor<P> { | ||
private final GeneralFormatStyle generalFormatStyle; | ||
|
||
@Nullable | ||
private final Tree stopAfter; | ||
|
||
public NormalizeLineBreaksVisitor(GeneralFormatStyle generalFormatStyle, @Nullable Tree stopAfter) { | ||
this.generalFormatStyle = generalFormatStyle; | ||
this.stopAfter = stopAfter; | ||
} | ||
|
||
@Override | ||
public @Nullable Json postVisit(Json tree, P p) { | ||
if (stopAfter != null && stopAfter.isScope(tree)) { | ||
getCursor().putMessageOnFirstEnclosing(Json.Document.class, "stop", true); | ||
} | ||
return super.postVisit(tree, p); | ||
} | ||
|
||
@Override | ||
public @Nullable Json visit(@Nullable Tree tree, P p) { | ||
if (getCursor().getNearestMessage("stop") != null) { | ||
return (Json) tree; | ||
} | ||
|
||
Json y = super.visit(tree, p); | ||
if (y != null) { | ||
String modifiedWs = normalizeNewLines(y.getPrefix().getWhitespace(), generalFormatStyle.isUseCRLFNewLines()); | ||
if (!y.getPrefix().getWhitespace().equals(modifiedWs)) { | ||
y = y.withPrefix(y.getPrefix().withWhitespace(modifiedWs)); | ||
} | ||
} | ||
return y; | ||
} | ||
} |
Oops, something went wrong.