Skip to content

Commit

Permalink
Autodetect formatting style of JSON code (#4916)
Browse files Browse the repository at this point in the history
* 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
5 people authored Jan 17, 2025
1 parent 1d96858 commit afa160e
Show file tree
Hide file tree
Showing 15 changed files with 1,199 additions and 6 deletions.
135 changes: 135 additions & 0 deletions rewrite-json/src/main/java/org/openrewrite/json/AddKeyValue.java
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;
}
};
}
}
30 changes: 30 additions & 0 deletions rewrite-json/src/main/java/org/openrewrite/json/JsonVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.openrewrite.SourceFile;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.ListUtils;
import org.openrewrite.json.format.AutoFormatVisitor;
import org.openrewrite.json.tree.Json;
import org.openrewrite.json.tree.JsonRightPadded;
import org.openrewrite.json.tree.JsonValue;
Expand All @@ -37,6 +38,35 @@ public String getLanguage() {
return "json";
}

public <Y2 extends Json> Y2 maybeAutoFormat(Y2 before, Y2 after, P p) {
return maybeAutoFormat(before, after, p, getCursor());
}

public <Y2 extends Json> Y2 maybeAutoFormat(Y2 before, Y2 after, P p, Cursor cursor) {
return maybeAutoFormat(before, after, null, p, cursor);
}

@SuppressWarnings({"unchecked", "ConstantConditions"})
public <Y2 extends Json> Y2 maybeAutoFormat(Y2 before, Y2 after, @Nullable Json stopAfter, P p, Cursor cursor) {
if (before != after) {
return (Y2) new AutoFormatVisitor<>(stopAfter).visit(after, p, cursor);
}
return after;
}

public <Y2 extends Json> Y2 autoFormat(Y2 y, P p) {
return autoFormat(y, p, getCursor());
}

public <Y2 extends Json> Y2 autoFormat(Y2 y, P p, Cursor cursor) {
return autoFormat(y, null, p, cursor);
}

@SuppressWarnings({"ConstantConditions", "unchecked"})
public <Y2 extends Json> Y2 autoFormat(Y2 y, @Nullable Json stopAfter, P p, Cursor cursor) {
return (Y2) new AutoFormatVisitor<>(stopAfter).visit(y, p, cursor);
}

public Json visitArray(Json.Array array, P p) {
Json.Array a = array;
a = a.withPrefix(visitSpace(a.getPrefix(), p));
Expand Down
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);
}
}
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;
}
}
}
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;
}
}
Loading

0 comments on commit afa160e

Please sign in to comment.