From 2ba94fb3999568ec177f06c061886736f0c261f0 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 23 Mar 2026 23:59:18 +0300 Subject: [PATCH 01/29] =?UTF-8?q?=D0=9F=D1=80=D0=B8=20=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=BA=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B7=D0=B0=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B9=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=88=D0=B8=D0=BD=D0=B5,=20=D1=82=D0=B5=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B8=D1=81=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D0=B8=D1=82=20=D0=B5=D1=91=20=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/ebnfFormatter/match/PatternMatcher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java index c39d17a..ba0562b 100644 --- a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java +++ b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java @@ -44,7 +44,7 @@ private boolean matchNodePat(NodePat pat, Object value, Bindings b) { return false; } - TypeSpec spec = typeRegistry.get(pat.typeName()); + TypeSpec spec = typeRegistry.requireByDslName(pat.typeName()); if (spec == null) { return false; } From d106e309819c2f83bd2201fc9bfb94cb63e04cf2 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 24 Mar 2026 00:38:00 +0300 Subject: [PATCH 02/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=82=D0=BE,=20=D1=87=D1=82=D0=BE=20RuleDef=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=B2=D0=B5=D1=81=D1=82=D0=B5=D0=BD=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BA=20DSL=20=D1=82=D0=B8=D0=BF,=20=D0=BD=D0=B0=20=D1=82?= =?UTF-8?q?=D0=BE,=20=D1=87=D1=82=D0=BE=20Node=20-=20=D1=8D=D0=BA=D0=B7?= =?UTF-8?q?=D0=B5=D0=BC=D0=BF=D0=BB=D1=8F=D1=80=20=D0=BD=D1=83=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B8=D0=BF=D0=B0=20=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=82=D0=BE,=20=D1=87=D1=82=D0=BE=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8A=D0=B5=D0=BA=D1=82=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=82=D0=B2=D1=83=D0=B5=D1=82=20Java=20=D1=82?= =?UTF-8?q?=D0=B8=D0=BF=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ebnfFormatter/match/PatternMatcher.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java index ba0562b..99fcbb6 100644 --- a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java +++ b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java @@ -3,6 +3,7 @@ import com.github.javaparser.ast.Node; import org.example.ebnfFormatter.model.pattern.*; import org.example.ebnfFormatter.runtime.TypeRegistry; +import org.example.ebnfFormatter.runtime.TypeRegistryUniversal; import org.example.ebnfFormatter.runtime.TypeSpec; import java.lang.reflect.Array; @@ -12,9 +13,9 @@ public final class PatternMatcher { - private final TypeRegistry typeRegistry; + private final TypeRegistryUniversal typeRegistry; - public PatternMatcher(TypeRegistry typeRegistry) { + public PatternMatcher(TypeRegistryUniversal typeRegistry) { this.typeRegistry = typeRegistry; } @@ -36,6 +37,28 @@ private boolean matchLit(Lit pat, Object value) { } private boolean matchRuleRef(RuleRef ref, Object value, Bindings bindings) { + if (value == null) { + return bindings.bind(ref.name(), null); + } + + TypeSpec spec; + try { + spec = typeRegistry.requireByDslName(ref.name()); + } catch (IllegalArgumentException e) { + return bindings.bind(ref.name(), value); + } + + if (value instanceof Node node) { + if (!spec.javaType().isInstance(node)) { + return false; + } + return bindings.bind(ref.name(), value); + } + + if (!spec.javaType().isInstance(value)) { + return false; + } + return bindings.bind(ref.name(), value); } From 6587b008dbbde2f821aaa9d40e4c781a4627f9ed Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 24 Mar 2026 00:41:56 +0300 Subject: [PATCH 03/29] =?UTF-8?q?=D0=A2=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D0=BC=D1=8B=20=D0=BF=D0=B8=D1=88=D0=B5=D0=BC=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0,=20=D0=BD=D0=B0=D0=B7=D1=8B?= =?UTF-8?q?=D0=B2=D0=B0=D1=8F=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=8B=20=D0=B2=20=D0=BD=D0=B8=D1=85,=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=B2=20Java=20Parser,=20=D0=B8=D0=B4=D1=91=D1=82=20=D0=B0?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D0=B9=20=D0=BC=D0=B0=D1=82=D1=87=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=20=D1=81=20=D0=BD=D0=B8=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/TestFormatterFactory.java | 2 +- .../runtime/TypeRegistryUniversal.java | 236 ++++++++++++++++++ .../runtime/FormatterEngineE2ETest.java | 2 +- 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java diff --git a/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java b/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java index f0f8a07..25d4569 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java @@ -22,7 +22,7 @@ public static FormatterEngine createEngine() { RuleRegistry ruleRegistry = new RuleRegistry(); ruleRegistry.registerAll(rulesFromDocumentation()); - TypeRegistry typeRegistry = new TypeRegistry(); + TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); PatternMatcher patternMatcher = new PatternMatcher(typeRegistry); TemplateRenderer templateRenderer = new TemplateRenderer(); diff --git a/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java b/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java new file mode 100644 index 0000000..d993170 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java @@ -0,0 +1,236 @@ +package org.example.ebnfFormatter.runtime; + +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.stmt.Statement; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +public final class TypeRegistryUniversal { + private static final List AST_PACKAGES = List.of( + "com.github.javaparser.ast", + "com.github.javaparser.ast.body", + "com.github.javaparser.ast.comments", + "com.github.javaparser.ast.expr", + "com.github.javaparser.ast.modules", + "com.github.javaparser.ast.stmt", + "com.github.javaparser.ast.type" + ); + + private final Map byDslName = new LinkedHashMap<>(); + private final Map, TypeSpec> byJavaType = new LinkedHashMap<>(); + + public TypeRegistryUniversal() { + registerAbstractAliases(); + } + + private void registerAbstractAliases() { + register(simpleType("Node", Node.class)); + register(simpleType("Statement", Statement.class)); + register(simpleType("Expression", Expression.class)); + } + + public TypeSpec requireByDslName(String dslName) { + TypeSpec existing = byDslName.get(dslName); + if (existing != null) { + return existing; + } + + Class javaType = resolveJavaParserType(dslName); + if (javaType == null) { + throw new IllegalArgumentException("Unknown DSL type: " + dslName); + } + + TypeSpec generated = buildTypeSpec(javaType); + register(generated); + return generated; + } + + public TypeSpec findByJavaType(Class javaType) { + TypeSpec exact = byJavaType.get(javaType); + if (exact != null) { + return exact; + } + + for (Map.Entry, TypeSpec> entry : byJavaType.entrySet()) { + if (entry.getKey().isAssignableFrom(javaType)) { + return entry.getValue(); + } + } + + if (Node.class.isAssignableFrom(javaType)) { + TypeSpec generated = buildTypeSpec(javaType); + register(generated); + return generated; + } + + return null; + } + + public void register(TypeSpec spec) { + TypeSpec prevDsl = byDslName.put(spec.dslName(), spec); + TypeSpec prevJava = byJavaType.put(spec.javaType(), spec); + + if (prevDsl != null && prevDsl != spec) { + throw new IllegalStateException("Duplicate DSL type: " + spec.dslName()); + } + if (prevJava != null && prevJava != spec) { + throw new IllegalStateException("Duplicate Java type: " + spec.javaType().getName()); + } + } + + public Object readProperty(Node node, String propertyName) { + TypeSpec spec = findByJavaType(node.getClass()); + if (spec == null) { + throw new IllegalArgumentException("No TypeSpec registered for " + node.getClass().getName()); + } + + PropertySpec property = spec.property(propertyName); + if (property == null) { + throw new IllegalArgumentException( + "Unknown property '" + propertyName + "' for DSL type " + spec.dslName() + ); + } + + Object raw = property.get(node); + return normalizePropertyValue(raw); + } + + public TypeSpec get(String dslName) { + return byDslName.get(dslName); + } + + private Class resolveJavaParserType(String dslName) { + for (String pkg : AST_PACKAGES) { + try { + Class cls = Class.forName(pkg + "." + dslName); + if (Node.class.isAssignableFrom(cls)) { + return cls; + } + } catch (ClassNotFoundException ignored) { + } + } + return null; + } + + private TypeSpec buildTypeSpec(Class javaType) { + Class nodeType = javaType.asSubclass(Node.class); + return new TypeSpec( + nodeType.getSimpleName(), + nodeType, + buildProperties(nodeType) + ); + } + + private Map buildProperties(Class nodeType) { + Map props = new LinkedHashMap<>(); + + for (Method method : nodeType.getMethods()) { + if (!isGetter(method)) { + continue; + } + + String name = getterToPropertyName(method); + if (isIgnoredProperty(name)) { + continue; + } + + Class returnType = method.getReturnType(); + + Function getter = node -> { + try { + return method.invoke(node); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Cannot read property '" + name + "' from " + nodeType.getName(), e + ); + } + }; + + PropertySpec propertySpec; + if (Optional.class.isAssignableFrom(returnType)) { + propertySpec = optional(name, getter); + } else if (NodeList.class.isAssignableFrom(returnType)) { + propertySpec = list(name, getter); + } else { + propertySpec = required(name, getter); + } + + props.putIfAbsent(name, propertySpec); + } + + return props; + } + + private boolean isGetter(Method method) { + if (Modifier.isStatic(method.getModifiers())) return false; + if (method.getParameterCount() != 0) return false; + if (method.getReturnType() == void.class) return false; + if (method.getDeclaringClass() == Object.class) return false; + + String name = method.getName(); + return (name.startsWith("get") && name.length() > 3) + || (name.startsWith("is") && name.length() > 2); + } + + private String getterToPropertyName(Method method) { + String name = method.getName(); + + if (name.startsWith("get")) { + return Character.toLowerCase(name.charAt(3)) + name.substring(4); + } + if (name.startsWith("is")) { + return Character.toLowerCase(name.charAt(2)) + name.substring(3); + } + + throw new IllegalArgumentException("Not a getter: " + method.getName()); + } + + private boolean isIgnoredProperty(String name) { + return switch (name) { + case "metaModel", + "range", + "tokenRange", + "parsed", + "comment", + "orphanComments", + "allContainedComments", + "childNodes", + "parentNode" -> true; + default -> false; + }; + } + + private static TypeSpec simpleType(String dslName, Class type) { + return new TypeSpec(dslName, type, Map.of()); + } + + private static PropertySpec required(String name, Function getter) { + return new PropertySpec(name, getter, false, false); + } + + private static PropertySpec optional(String name, Function getter) { + return new PropertySpec(name, getter, true, false); + } + + private static PropertySpec list(String name, Function getter) { + return new PropertySpec(name, getter, false, true); + } + + public static Object normalizePropertyValue(Object value) { + if (value instanceof Optional optional) { + return optional.orElse(null); + } + if (value instanceof NodeList nodeList) { + return nodeList; + } + return value; + } +} \ No newline at end of file diff --git a/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java b/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java index 7df124f..47f0261 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java @@ -265,7 +265,7 @@ private static FormatterEngine engineWithRules(RuleDef... rules) { RuleRegistry ruleRegistry = new RuleRegistry(); ruleRegistry.registerAll(List.of(rules)); - TypeRegistry typeRegistry = new TypeRegistry(); + TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); PatternMatcher patternMatcher = new PatternMatcher(typeRegistry); TemplateRenderer templateRenderer = new TemplateRenderer(); From aea3982a91a7a4fcd7a20638da61c71c8f216257 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 24 Mar 2026 00:43:19 +0300 Subject: [PATCH 04/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20DSL=20+=20=D0=BA=D0=BE=D0=B4=20=3D>?= =?UTF-8?q?=20=D0=BE=D1=82=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?,=20=D0=B1=D0=BE=D0=BB=D0=B5=D0=B5=20=D1=81=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B1=D1=83=D0=B4=D1=83=D1=82=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D1=88=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FromRulesAndCodeToFormattedCodeTest.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java diff --git a/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java b/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java new file mode 100644 index 0000000..347ed99 --- /dev/null +++ b/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java @@ -0,0 +1,68 @@ +package org.example.ebnfFormatter.runtime; + + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.stmt.ForStmt; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.example.ebnfFormatter.dsl.RuleAstBuilder; +import org.example.ebnfFormatter.match.PatternMatcher; +import org.example.ebnfFormatter.model.RuleDef; +import org.example.ebnfFormatter.render.TemplateRenderer; +import org.example.ebnfLexer; +import org.example.ebnfParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + + +import static org.assertj.core.api.Assertions.assertThat; + +public class FromRulesAndCodeToFormattedCodeTest { + @Test + void endToEndForStmt() { + String rules = """ + ::= ForStmt + => "for"; + """; + + String code = """ + public class AST { + void m() { + for (;;) make(); + } + } + """; + + List parsed = parseRules(rules); + + RuleRegistry ruleRegistry = new RuleRegistry(); + ruleRegistry.registerAll(parsed); + + TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); + PatternMatcher patternMatcher = new PatternMatcher(typeRegistry); + TemplateRenderer templateRenderer = new TemplateRenderer(); + FormatterEngine engine = new FormatterEngine(ruleRegistry, patternMatcher, templateRenderer); + + ForStmt node = StaticJavaParser.parse(code) + .findFirst(ForStmt.class) + .orElseThrow(() -> new AssertionError("ForStmt not found")); + + String formatted = engine.format(node, "ForStmt"); + + assertThat(formatted).isEqualTo("for"); + } + + private List parseRules(String text) { + ebnfLexer lexer = new ebnfLexer(CharStreams.fromString(text)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ebnfParser parser = new ebnfParser(tokens); + + ebnfParser.RulelistContext ctx = parser.rulelist(); + + RuleAstBuilder builder = new RuleAstBuilder(); + return builder.buildRules(ctx); + } + + +} From 14ff3a5ae7032c136e007ce80201fff27a46f302 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 31 Mar 2026 18:04:17 +0300 Subject: [PATCH 05/29] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D1=81=20=D0=BE=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=BC=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BC=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=20=D0=B1=D1=8B=D0=BB=D0=BE=20=D0=BF=D0=B8?= =?UTF-8?q?=D1=81=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D1=80=D0=B0=D0=B7=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B9=20(=D0=BD=D0=B0=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B6=D0=B5=D1=82=20=D0=B1=D1=8B=D1=82=D1=8C=20?= =?UTF-8?q?=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=BD=D1=8B=D1=85=20=D1=82=D0=B8=D0=BF=D0=BE=D0=B2?= =?UTF-8?q?=20Statement)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/FormatterEngine.java | 17 +++++++------ .../ebnfFormatter/runtime/RuleRegistry.java | 24 ++++++++++++------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java b/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java index 5e445e7..e0e3ccf 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java @@ -22,15 +22,14 @@ public FormatterEngine( } public String format(Node node, String ruleName) { - RuleDef rule = ruleRegistry.require(ruleName); - MatchResult match = patternMatcher.match(rule.pattern(), node); - - if (!match.matched()) { - throw new IllegalArgumentException( - "Node does not match rule <" + ruleName + ">" - ); + for (RuleDef rule : ruleRegistry.requireAll(ruleName)) { + MatchResult match = patternMatcher.match(rule.pattern(), node); + if (match.matched()) { + return templateRenderer.render(rule.format(), match.bindings()); + } } - - return templateRenderer.render(rule.format(), match.bindings()); + throw new IllegalArgumentException( + "Node does not match any rule <" + ruleName + ">" + ); } } \ No newline at end of file diff --git a/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java b/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java index fecaa0c..5968328 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java @@ -2,15 +2,13 @@ import org.example.ebnfFormatter.model.RuleDef; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; public final class RuleRegistry { - private final Map rulesByName = new HashMap<>(); + private final Map> rulesByName = new HashMap<>(); public void register(RuleDef rule) { - rulesByName.put(rule.name(), rule); + rulesByName.computeIfAbsent(rule.name(), k -> new ArrayList<>()).add(rule); } public void registerAll(Collection rules) { @@ -19,16 +17,24 @@ public void registerAll(Collection rules) { } } - public RuleDef require(String ruleName) { - RuleDef rule = rulesByName.get(ruleName); + public List require(String ruleName) { + List rule = rulesByName.get(ruleName); if (rule == null) { throw new IllegalArgumentException("Unknown rule: " + ruleName); } return rule; } - public RuleDef find(String ruleName) { - return rulesByName.get(ruleName); + public List requireAll(String ruleName) { + List rules = rulesByName.get(ruleName); + if (rules == null || rules.isEmpty()) { + throw new IllegalArgumentException("Unknown rule: " + ruleName); + } + return rules; + } + + public List findAll(String ruleName) { + return rulesByName.getOrDefault(ruleName, List.of()); } public boolean contains(String ruleName) { From 74c8b12c845209ae7e44709b9d320ac4ec2661cc Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 31 Mar 2026 18:18:24 +0300 Subject: [PATCH 06/29] del old func require --- .../org/example/ebnfFormatter/runtime/RuleRegistry.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java b/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java index 5968328..be6919c 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/RuleRegistry.java @@ -17,14 +17,6 @@ public void registerAll(Collection rules) { } } - public List require(String ruleName) { - List rule = rulesByName.get(ruleName); - if (rule == null) { - throw new IllegalArgumentException("Unknown rule: " + ruleName); - } - return rule; - } - public List requireAll(String ruleName) { List rules = rulesByName.get(ruleName); if (rules == null || rules.isEmpty()) { From 303069a83af3482a4c121747ef58d4d62c45a35d Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 31 Mar 2026 19:51:12 +0300 Subject: [PATCH 07/29] =?UTF-8?q?=D0=9D=D0=B0=20=D0=BE=D1=81=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B5=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B0=D0=B7=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D1=8B=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=81=20?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D0=B8=D0=BC=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=81=D0=BE=D0=BE=D1=82=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=B2=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BD=D0=BA=D1=80=D0=B5=D1=82?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D1=83=20=D0=B8=D0=B7=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0,?= =?UTF-8?q?=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=BF=D0=BE=D0=B3=D0=BE=20bind,=20=D0=BA=D0=B0=D0=BA=20=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=8C=D1=88=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ebnfFormatter/match/PatternMatcher.java | 37 ++++++++++++------- .../runtime/TestFormatterFactory.java | 2 +- .../runtime/FormatterEngineE2ETest.java | 2 +- .../FromRulesAndCodeToFormattedCodeTest.java | 2 +- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java index 99fcbb6..7c5f34d 100644 --- a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java +++ b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java @@ -1,7 +1,9 @@ package org.example.ebnfFormatter.match; import com.github.javaparser.ast.Node; +import org.example.ebnfFormatter.model.RuleDef; import org.example.ebnfFormatter.model.pattern.*; +import org.example.ebnfFormatter.runtime.RuleRegistry; import org.example.ebnfFormatter.runtime.TypeRegistry; import org.example.ebnfFormatter.runtime.TypeRegistryUniversal; import org.example.ebnfFormatter.runtime.TypeSpec; @@ -14,9 +16,11 @@ public final class PatternMatcher { private final TypeRegistryUniversal typeRegistry; + private final RuleRegistry ruleRegistry; - public PatternMatcher(TypeRegistryUniversal typeRegistry) { + public PatternMatcher(TypeRegistryUniversal typeRegistry, RuleRegistry ruleRegistry) { this.typeRegistry = typeRegistry; + this.ruleRegistry = ruleRegistry; } private boolean matchFork(PatternAst pattern, Object value, Bindings b) { @@ -37,28 +41,35 @@ private boolean matchLit(Lit pat, Object value) { } private boolean matchRuleRef(RuleRef ref, Object value, Bindings bindings) { + List rules = ruleRegistry.findAll(ref.name()); + + if (!rules.isEmpty()) { + for (RuleDef rule : rules) { + Bindings copy = bindings.copy(); + if (matchFork(rule.pattern(), value, copy)) { + if (!copy.bind(ref.name(), value)) { + return false; + } + bindings.replaceWith(copy); + return true; + } + } + return false; + } + if (value == null) { return bindings.bind(ref.name(), null); } - TypeSpec spec; try { - spec = typeRegistry.requireByDslName(ref.name()); - } catch (IllegalArgumentException e) { - return bindings.bind(ref.name(), value); - } - - if (value instanceof Node node) { - if (!spec.javaType().isInstance(node)) { + TypeSpec spec = typeRegistry.requireByDslName(ref.name()); + if (!spec.javaType().isInstance(value)) { return false; } + } catch (IllegalArgumentException e) { return bindings.bind(ref.name(), value); } - if (!spec.javaType().isInstance(value)) { - return false; - } - return bindings.bind(ref.name(), value); } diff --git a/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java b/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java index 25d4569..6229c37 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/TestFormatterFactory.java @@ -23,7 +23,7 @@ public static FormatterEngine createEngine() { ruleRegistry.registerAll(rulesFromDocumentation()); TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); - PatternMatcher patternMatcher = new PatternMatcher(typeRegistry); + PatternMatcher patternMatcher = new PatternMatcher(typeRegistry, ruleRegistry); TemplateRenderer templateRenderer = new TemplateRenderer(); return new FormatterEngine(ruleRegistry, patternMatcher, templateRenderer); diff --git a/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java b/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java index 47f0261..eb59897 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/FormatterEngineE2ETest.java @@ -266,7 +266,7 @@ private static FormatterEngine engineWithRules(RuleDef... rules) { ruleRegistry.registerAll(List.of(rules)); TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); - PatternMatcher patternMatcher = new PatternMatcher(typeRegistry); + PatternMatcher patternMatcher = new PatternMatcher(typeRegistry, ruleRegistry); TemplateRenderer templateRenderer = new TemplateRenderer(); return new FormatterEngine(ruleRegistry, patternMatcher, templateRenderer); diff --git a/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java b/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java index 347ed99..ee4d68b 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java @@ -40,7 +40,7 @@ void m() { ruleRegistry.registerAll(parsed); TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); - PatternMatcher patternMatcher = new PatternMatcher(typeRegistry); + PatternMatcher patternMatcher = new PatternMatcher(typeRegistry, ruleRegistry); TemplateRenderer templateRenderer = new TemplateRenderer(); FormatterEngine engine = new FormatterEngine(ruleRegistry, patternMatcher, templateRenderer); From fdd0b001a0b02b06f04a0deae046f132d38c64ce Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 31 Mar 2026 19:57:43 +0300 Subject: [PATCH 08/29] =?UTF-8?q?spec=20=D0=BD=D0=B5=20=D0=B1=D1=8B=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D1=82=20null,=20readProperty=20=D0=BB=D0=B8=D0=B1?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=B8=D0=B4=D0=B0=D0=B5=D1=82=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BA=D1=83,=20=D0=BB=D0=B8=D0=B1=D0=BE=20=D0=B2?= =?UTF-8?q?=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D1=82=20=D1=87?= =?UTF-8?q?=D1=82=D0=BE-=D1=82=D0=BE=20=D0=BE=D1=81=D0=BC=D1=8B=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=BD=D0=BE=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/ebnfFormatter/match/PatternMatcher.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java index 7c5f34d..b41990b 100644 --- a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java +++ b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java @@ -79,10 +79,6 @@ private boolean matchNodePat(NodePat pat, Object value, Bindings b) { } TypeSpec spec = typeRegistry.requireByDslName(pat.typeName()); - if (spec == null) { - return false; - } - if (!spec.javaType().isInstance(node)) { return false; } From 7a287c42f523168ce7812175d7eab62f44bd0933 Mon Sep 17 00:00:00 2001 From: Igor Date: Tue, 31 Mar 2026 20:00:58 +0300 Subject: [PATCH 09/29] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=B0=D1=8F=20property=20=D0=BD=D0=B5=20=D0=B1?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20null,=20=D1=84=D1=83=D0=BD?= =?UTF-8?q?=D0=BA=D1=86=D0=B8=D1=8F=20property=20=D0=BB=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=20=D0=BA=D0=B8=D0=B4=D0=B0=D0=B5=D1=82=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D1=83,=20=D0=BB=D0=B8=D0=B1=D0=BE=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=89=D0=B0=D0=B5=D1=82=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE-=D1=82=D0=BE=20=D0=BE=D1=81=D0=BC=D1=8B=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D0=BE=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/ebnfFormatter/runtime/TypeRegistryUniversal.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java b/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java index d993170..1fc2278 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/TypeRegistryUniversal.java @@ -93,11 +93,6 @@ public Object readProperty(Node node, String propertyName) { } PropertySpec property = spec.property(propertyName); - if (property == null) { - throw new IllegalArgumentException( - "Unknown property '" + propertyName + "' for DSL type " + spec.dslName() - ); - } Object raw = property.get(node); return normalizePropertyValue(raw); From 67325527b0342d26f91408842ff4bfaa75deabe3 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 1 Apr 2026 00:21:11 +0300 Subject: [PATCH 10/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=BB=D0=BD=D0=BE=D1=86=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=87=D1=82=D0=BE=20=D0=BD=D0=B5=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BC=D0=BC=D0=B0=D1=82=D0=B8=D0=BA=D0=B0=20=D0=B2?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=20=20=D0=B8=20,=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B0=20=D1=8D=D1=82=D0=BE=20?= =?UTF-8?q?=D0=B1=D1=83=D0=B4=D1=83=D1=82=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=B7=D0=B6=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FromRulesAndCodeToFormattedCodeTest.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java b/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java index ee4d68b..e80f555 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/FromRulesAndCodeToFormattedCodeTest.java @@ -22,8 +22,17 @@ public class FromRulesAndCodeToFormattedCodeTest { @Test void endToEndForStmt() { String rules = """ - ::= ForStmt - => "for"; + ::= ForStmt(body=) + => "for" sp "(" ";" ";" ")" nl indent dedent; + + ::= ExpressionStmt(expression=) + => ";"; + + ::= MethodCallExpr(name=) + => "(" ")"; + + ::= SimpleName(identifier="make") + => "make"; """; String code = """ @@ -34,6 +43,11 @@ void m() { } """; + String expected = """ + for (;;) + make(); + """.trim(); + List parsed = parseRules(rules); RuleRegistry ruleRegistry = new RuleRegistry(); @@ -50,7 +64,7 @@ void m() { String formatted = engine.format(node, "ForStmt"); - assertThat(formatted).isEqualTo("for"); + assertThat(formatted).isEqualTo(expected); } private List parseRules(String text) { From 928ea6ffc89d85a6d2f35103672e0ba1f7f275aa Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 1 Apr 2026 15:22:08 +0300 Subject: [PATCH 11/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B8=D0=B7=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8,=20=20=D0=B8=20=20=D0=BF=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20=D1=87=D1=82=D0=BE=20=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B4=D0=B5=D1=80=D0=B6=D0=B8=D0=B2=D0=B0=D1=8E=D1=82=D1=81?= =?UTF-8?q?=D1=8F,=20=D1=82=D0=B0=D0=BA=20=D1=87=D1=82=D0=BE=20=D1=82?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D1=8B=20=D1=81=20forStmt=20=D0=B8=20methodDe?= =?UTF-8?q?claration=20=D0=BF=D0=BE=D0=BA=D0=B0=20=D0=B7=D0=B0=D1=85=D0=B0?= =?UTF-8?q?=D1=80=D0=B4=D0=BA=D0=BE=D0=B6=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExamplesFromDocumentationTest.java | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java new file mode 100644 index 0000000..b2e455f --- /dev/null +++ b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java @@ -0,0 +1,343 @@ +package org.example.ebnfFormatter.runtime; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.stmt.ForStmt; +import com.github.javaparser.ast.stmt.IfStmt; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.example.ebnfFormatter.dsl.RuleAstBuilder; +import org.example.ebnfFormatter.match.PatternMatcher; +import org.example.ebnfFormatter.model.RuleDef; +import org.example.ebnfFormatter.render.TemplateRenderer; +import org.example.ebnfLexer; +import org.example.ebnfParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExamplesFromDocumentationTest { + private static final String ifStmtRules = """ + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" sp + ifpresent(Statement?, sp "else" sp ); + """; + + @Test + void ifStmtExample1() { + String code = """ + public class AST { + public int sum(int a, int b) { + if(a == b){return 2 * a;} + return a + b; + } + } + """; + + String expected = """ + if (a == b) { + return 2 * a; + }"""; + +String formatted = formatFirstNode(ifStmtRules, code, IfStmt.class, "IfStmt"); + assertThat(formatted).isEqualTo(expected); + } + + @Test + void ifStmtExample2() { + String rules = """ + ::= IfStmt(condition=, thenStmt=, elseStmt=) + => "if" sp "(" ")" nl indent dedent + nl "else" sp nl indent dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= IfStmt + => ; + """; + + String code = """ + public class AST { + public int sum(int a, int b) { + if(a == b)return 2 * a; + else return a + b; + } + } + """; + + String expected = """ + if (a == b) + return 2 * a; + else\s + return a + b;"""; + + String formatted = formatFirstNode(rules, code, IfStmt.class, "IfStmt"); + assertThat(formatted).isEqualTo(expected); + } + + @Test + void ifStmtExample3() { + String rules = """ + ::= IfStmt(condition=, thenStmt=, elseStmt=) + => "if" sp "(" ")" nl indent dedent + nl "else" sp nl indent dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= IfStmt + => ; + """; + + String code = """ + public class AST { + public int sum(int a, int b) { + if(a == b)return 2 * a; + else if((a&2)==2) return a + b; + else return b; + } + } + """; + + String expected = """ + if (a == b) + return 2 * a; + else\s + if ((a & 2) == 2) + return a + b; + else + return b;"""; + + String formatted = formatFirstNode(rules, code, IfStmt.class, "IfStmt"); + assertThat(formatted).isEqualTo(expected); + } +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// private static final String methodDeclarationRules = """ +// ::= MethodDeclaration( +// modifiers=[], +// type=, +// name=, +// parameters=[], +// body?=) +// => join(, sp) sp sp +// "(" ifpresent(Parameter*, join(, ", ")) ")" +// ifpresent(Statement?, sp ); +// """; + + @Test + void methodDeclarationExample1() { + String rules = """ + ::= MethodDeclaration(body=BlockStmt) + => "public" sp "int" sp "sum" "(" "int a, int b" ")" sp "{" + nl indent "if(a==b){return 2*a;}return a+b;" nl dedent "}"; + """; + + String code = """ + public class AST { + public int sum(int a,int b){if(a==b){return 2*a;}return a+b;} + } + """; + + String expected = """ + public int sum(int a, int b) { + if(a==b){return 2*a;}return a+b; + }"""; + + String formatted = formatFirstNode(rules, code, MethodDeclaration.class, "MethodDeclaration"); + assertThat(formatted).isEqualTo(expected); + } + + @Test + void methodDeclarationExample2() { + String rules = """ + ::= MethodDeclaration(body=BlockStmt) + => "public" sp "int" sp "sum" "(" + "Parameter a, Parameter b, Parameter c, Parameter d" + ")" sp "{" nl indent nl dedent "}"; + """; + + String code = """ + public class AST { + public int sum(Parameter a,Parameter b,Parameter c,Parameter d) {} + } + """; + + String expected = """ + public int sum(Parameter a, Parameter b, Parameter c, Parameter d) { + + }"""; + + String formatted = formatFirstNode(rules, code, MethodDeclaration.class, "MethodDeclaration"); + assertThat(formatted).isEqualTo(expected); + } + + @Test + void methodDeclarationExample3() { + String rules = """ + ::= MethodDeclaration + => "public" sp "abstract" sp "int" sp "sum" "(" "Input input" ")" ";"; + """; + + + String code = """ + abstract class AST { + public abstract int sum(Input + input); + } + """; + + String expected = "public abstract int sum(Input input);"; + + String formatted = formatFirstNode(rules, code, MethodDeclaration.class, "MethodDeclaration"); + assertThat(formatted).isEqualTo(expected); + } + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// private static final String forStmtRules = """ +// ::= ForStmt( +// initialization=[], +// compare?=, +// update=[], +// body= +// ) +// => "for" sp "(" +// ifpresent(Expression*, join(, ", ")) +// ";" ifpresent(Expression?, sp ) +// ";" ifpresent(Expression*, sp join(, ", ")) +// ")" sp ; +// +// ::= BlockStmt(statements=[]) +// => "{" nl indent join(, nl) nl dedent "}"; +// +// ::= ExpressionStmt(expression=) +// => nl indent ";" dedent; +// +// ::= ReturnStmt(expression=) +// => nl indent "return" sp ";" dedent; +// """; + @Test + void forStmtExample1() { + String rules = """ + ::= ForStmt(body=BlockStmt) + => "for" sp "(" "int i=0" ";" sp "i<5" ";" sp "++i" ")" sp "{" + nl indent "sm += i;" nl dedent "}"; + """; + + String code = """ + public class AST { + public int sum(int a, int b) { + int sm = 0; + for (int i=0;i<5;++i){sm += i;} + } + } + """; + + String expected = """ + for (int i=0; i<5; ++i) { + sm += i; + }"""; + + String formatted = formatFirstNode(rules, code, ForStmt.class, "ForStmt"); + assertThat(formatted).isEqualTo(expected); + } + + @Test + void forStmtExample2() { + String rules = """ + ::= ForStmt(body=ExpressionStmt) + => "for" sp "(" "int i=0" ";" sp "i<5" ";" sp "++i" ")" nl indent "sm += i;" dedent; + """; + + String code = """ + public class AST { + public int sum(int a, int b) { + int sm = 0; + for (int i=0;i<5;++i)sm += i; + } + } + """; + + String expected = """ + for (int i=0; i<5; ++i) + sm += i;"""; + + String formatted = formatFirstNode(rules, code, ForStmt.class, "ForStmt"); + assertThat(formatted).isEqualTo(expected); + } + + @Test + void forStmtExample3() { + String rules = """ + ::= ForStmt(body=) + => "for" sp "(" ";" ";" ")" nl indent dedent; + + ::= ExpressionStmt(expression=) + => ";"; + + ::= MethodCallExpr(name=) + => "(" ")"; + + ::= SimpleName(identifier="make") + => "make"; + """; + + String code = """ + public class AST { + public int sum(int a, int b) { + for (;;)make(); + } + } + """; + + String expected = """ + for (;;) + make();"""; + + String formatted = formatFirstNode(rules, code, ForStmt.class, "ForStmt"); + assertThat(formatted).isEqualTo(expected); + } + + private String formatFirstNode( + String rules, + String code, + Class nodeClass, + String ruleName + ) { + List parsed = parseRules(rules); + + RuleRegistry ruleRegistry = new RuleRegistry(); + ruleRegistry.registerAll(parsed); + + TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); + PatternMatcher patternMatcher = new PatternMatcher(typeRegistry, ruleRegistry); + TemplateRenderer templateRenderer = new TemplateRenderer(); + FormatterEngine engine = new FormatterEngine(ruleRegistry, patternMatcher, templateRenderer); + + T node = StaticJavaParser.parse(code) + .findFirst(nodeClass) + .orElseThrow(() -> new AssertionError(nodeClass.getSimpleName() + " not found")); + + return engine.format(node, ruleName); + } + + private List parseRules(String text) { + ebnfLexer lexer = new ebnfLexer(CharStreams.fromString(text)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ebnfParser parser = new ebnfParser(tokens); + + ebnfParser.RulelistContext ctx = parser.rulelist(); + + RuleAstBuilder builder = new RuleAstBuilder(); + return builder.buildRules(ctx); + } +} \ No newline at end of file From 2b775ae1d66b05f7853e4048e2c8022a2fba4e80 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Apr 2026 11:51:31 +0300 Subject: [PATCH 12/29] =?UTF-8?q?=D0=92=D1=8B=D1=88=D0=BB=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= =?UTF-8?q?=20gradle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.properties | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bad7c24..f50f69e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip -networkTimeout=10000 -validateDistributionUrl=true +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 3dece05e0dea3591bb363e9b4cd45bf186b28a61 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Apr 2026 16:11:03 +0300 Subject: [PATCH 13/29] =?UTF-8?q?=D0=92=D0=B2=D0=BE=D0=B6=D1=83=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=86=D0=B5=D0=BF=D1=86=D0=B8=D1=8E:=20=D0=B5?= =?UTF-8?q?=D1=81=D0=BB=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=88=D0=B8=D0=BD=D1=8B=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=20DSL=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=BE?= =?UTF-8?q?=20=D0=B8=20=D0=BF=D0=BE=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=20bindings,=20=D1=82=D0=BE=20=D0=B5=D1=81=D1=82=D1=8C=20?= =?UTF-8?q?AppliedRuleValue,=20=D0=B5=D1=81=D0=BB=D0=B8=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=82=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20DSL=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0,=20?= =?UTF-8?q?=D1=82=D0=BE=20=D1=8D=D1=82=D0=BE=20RawValue=20=D0=B8=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80=D0=B0=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B8=D1=81=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=20=D0=BF=D0=BE=20=D0=B7=D0=B0=D0=B3=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=BD=D0=BE=D0=BC=D1=83=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D1=83=20=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20.toString(),=20=D0=B5=D1=81?= =?UTF-8?q?=D0=BB=D0=B8=20=D1=82=D0=B0=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=BD?= =?UTF-8?q?=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/example/ebnfFormatter/match/AppliedRule.java | 10 ++++++++++ .../example/ebnfFormatter/match/AppliedRuleValue.java | 8 ++++++++ .../org/example/ebnfFormatter/match/BoundValue.java | 5 +++++ .../java/org/example/ebnfFormatter/match/RawValue.java | 8 ++++++++ 4 files changed, 31 insertions(+) create mode 100644 src/main/java/org/example/ebnfFormatter/match/AppliedRule.java create mode 100644 src/main/java/org/example/ebnfFormatter/match/AppliedRuleValue.java create mode 100644 src/main/java/org/example/ebnfFormatter/match/BoundValue.java create mode 100644 src/main/java/org/example/ebnfFormatter/match/RawValue.java diff --git a/src/main/java/org/example/ebnfFormatter/match/AppliedRule.java b/src/main/java/org/example/ebnfFormatter/match/AppliedRule.java new file mode 100644 index 0000000..39de23f --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/match/AppliedRule.java @@ -0,0 +1,10 @@ +package org.example.ebnfFormatter.match; + +import org.example.ebnfFormatter.model.RuleDef; + +public record AppliedRule( + String logicalName, + RuleDef rule, + Object sourceValue, + Bindings bindings +) {} \ No newline at end of file diff --git a/src/main/java/org/example/ebnfFormatter/match/AppliedRuleValue.java b/src/main/java/org/example/ebnfFormatter/match/AppliedRuleValue.java new file mode 100644 index 0000000..7c4a330 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/match/AppliedRuleValue.java @@ -0,0 +1,8 @@ +package org.example.ebnfFormatter.match; + +public record AppliedRuleValue(AppliedRule appliedRule) implements BoundValue { + @Override + public Object legacyValue() { + return appliedRule.sourceValue(); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/ebnfFormatter/match/BoundValue.java b/src/main/java/org/example/ebnfFormatter/match/BoundValue.java new file mode 100644 index 0000000..9dff89d --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/match/BoundValue.java @@ -0,0 +1,5 @@ +package org.example.ebnfFormatter.match; + +public sealed interface BoundValue permits RawValue, AppliedRuleValue { + Object legacyValue(); +} \ No newline at end of file diff --git a/src/main/java/org/example/ebnfFormatter/match/RawValue.java b/src/main/java/org/example/ebnfFormatter/match/RawValue.java new file mode 100644 index 0000000..a26b7c8 --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/match/RawValue.java @@ -0,0 +1,8 @@ +package org.example.ebnfFormatter.match; + +public record RawValue(Object value) implements BoundValue { + @Override + public Object legacyValue() { + return value; + } +} From 05c58d3cf47b66fb98a2efee287d7a20aca229cc Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Apr 2026 16:12:35 +0300 Subject: [PATCH 14/29] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20=D1=8D=D1=82=D0=BE=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=86=D0=B5=D0=BF=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/ebnfFormatter/match/Bindings.java | 123 +++++++- .../ebnfFormatter/match/MatchResult.java | 17 +- .../ebnfFormatter/match/PatternMatcher.java | 294 +++++++++++------- .../render/NestedRuleRenderer.java | 8 + .../render/TemplateRenderer.java | 177 +++++++++-- .../runtime/FormatterEngine.java | 30 +- 6 files changed, 481 insertions(+), 168 deletions(-) create mode 100644 src/main/java/org/example/ebnfFormatter/render/NestedRuleRenderer.java diff --git a/src/main/java/org/example/ebnfFormatter/match/Bindings.java b/src/main/java/org/example/ebnfFormatter/match/Bindings.java index 02b7937..32963d1 100644 --- a/src/main/java/org/example/ebnfFormatter/match/Bindings.java +++ b/src/main/java/org/example/ebnfFormatter/match/Bindings.java @@ -1,59 +1,158 @@ package org.example.ebnfFormatter.match; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; public final class Bindings { - private final Map bindingsByName = new LinkedHashMap<>(); + private final Map> bindingsByName = new LinkedHashMap<>(); public boolean bind(String name, Object value) { + return bind(name, new RawValue(value)); + } + + public boolean bind(String name, BoundValue value) { if (!bindingsByName.containsKey(name)) { - bindingsByName.put(name, value); + bindingsByName.put(name, new ArrayList<>(List.of(value))); return true; } - return Objects.equals(bindingsByName.get(name), value); + + List existing = bindingsByName.get(name); + return existing.size() == 1 && Objects.equals(existing.getFirst(), value); + } + + public boolean bindAll(String name, List values) { + if (!bindingsByName.containsKey(name)) { + bindingsByName.put(name, new ArrayList<>(values)); + return true; + } + + return Objects.equals(bindingsByName.get(name), values); + } + + public void append(String name, BoundValue value) { + bindingsByName.computeIfAbsent(name, ignored -> new ArrayList<>()).add(value); + } + + public void appendAll(Bindings other) { + for (Map.Entry> entry : other.bindingsByName.entrySet()) { + bindingsByName + .computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()) + .addAll(entry.getValue()); + } } public Bindings copy() { Bindings copy = new Bindings(); - copy.bindingsByName.putAll(this.bindingsByName); + for (Map.Entry> entry : bindingsByName.entrySet()) { + copy.bindingsByName.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } return copy; } public void replaceWith(Bindings other) { bindingsByName.clear(); - bindingsByName.putAll(other.bindingsByName); + for (Map.Entry> entry : other.bindingsByName.entrySet()) { + bindingsByName.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } } public Object getRequired(String name) { - if (!bindingsByName.containsKey(name)) { + List values = findValuesInternal(name); + if (values == null) { throw new IllegalArgumentException("No binding for name: " + name); } - return bindingsByName.get(name); + return unwrapForLegacyUse(values); } public Object find(String name) { - return bindingsByName.get(name); + List values = findValuesInternal(name); + if (values == null || values.isEmpty()) { + return null; + } + return unwrapForLegacyUse(values); + } + + public List getRequiredValues(String name) { + List values = findValuesInternal(name); + if (values == null) { + throw new IllegalArgumentException("No binding for name: " + name); + } + return List.copyOf(values); + } + + public List findValues(String name) { + List values = findValuesInternal(name); + if (values == null) { + return List.of(); + } + return List.copyOf(values); } public boolean hasBinding(String name) { - return bindingsByName.containsKey(name); + return findValuesInternal(name) != null; } public Set getBindingNames() { return Collections.unmodifiableSet(bindingsByName.keySet()); } - public Map asUnmodifiableMap() { - return Collections.unmodifiableMap(bindingsByName); + public Map> asUnmodifiableMap() { + Map> copy = new LinkedHashMap<>(); + for (Map.Entry> entry : bindingsByName.entrySet()) { + copy.put(entry.getKey(), List.copyOf(entry.getValue())); + } + return Collections.unmodifiableMap(copy); } @Override public String toString() { return bindingsByName.toString(); } -} \ No newline at end of file + + private List findValuesInternal(String name) { + List exact = bindingsByName.get(name); + if (exact != null) { + return exact; + } + + String normalized = stripQuantifierSuffix(name); + if (normalized.equals(name)) { + return null; + } + return bindingsByName.get(normalized); + } + + private String stripQuantifierSuffix(String name) { + if (name == null || name.isEmpty()) { + return name; + } + + char last = name.charAt(name.length() - 1); + return switch (last) { + case '?', '*', '+' -> name.substring(0, name.length() - 1); + default -> name; + }; + } + + private Object unwrapForLegacyUse(List values) { + if (values.isEmpty()) { + return null; + } + + if (values.size() == 1) { + return values.getFirst().legacyValue(); + } + + List legacyValues = new ArrayList<>(values.size()); + for (BoundValue value : values) { + legacyValues.add(value.legacyValue()); + } + return legacyValues; + } +} diff --git a/src/main/java/org/example/ebnfFormatter/match/MatchResult.java b/src/main/java/org/example/ebnfFormatter/match/MatchResult.java index 965d557..f3ac4a0 100644 --- a/src/main/java/org/example/ebnfFormatter/match/MatchResult.java +++ b/src/main/java/org/example/ebnfFormatter/match/MatchResult.java @@ -1,11 +1,18 @@ package org.example.ebnfFormatter.match; -public record MatchResult(boolean matched, Bindings bindings) { - public static MatchResult success(Bindings bindings) { - return new MatchResult(true, bindings); +public record MatchResult(boolean matched, AppliedRule appliedRule) { + public static MatchResult success(AppliedRule appliedRule) { + return new MatchResult(true, appliedRule); } public static MatchResult failure() { - return new MatchResult(false, new Bindings()); + return new MatchResult(false, null); } -} \ No newline at end of file + + public Bindings bindings() { + if (!matched || appliedRule == null) { + return new Bindings(); + } + return appliedRule.bindings(); + } +} diff --git a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java index b41990b..bb4c0b4 100644 --- a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java +++ b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java @@ -2,9 +2,16 @@ import com.github.javaparser.ast.Node; import org.example.ebnfFormatter.model.RuleDef; -import org.example.ebnfFormatter.model.pattern.*; +import org.example.ebnfFormatter.model.pattern.Alt; +import org.example.ebnfFormatter.model.pattern.FieldPat; +import org.example.ebnfFormatter.model.pattern.ListPat; +import org.example.ebnfFormatter.model.pattern.Lit; +import org.example.ebnfFormatter.model.pattern.NodePat; +import org.example.ebnfFormatter.model.pattern.PatternAst; +import org.example.ebnfFormatter.model.pattern.Quant; +import org.example.ebnfFormatter.model.pattern.RuleRef; +import org.example.ebnfFormatter.model.pattern.Seq; import org.example.ebnfFormatter.runtime.RuleRegistry; -import org.example.ebnfFormatter.runtime.TypeRegistry; import org.example.ebnfFormatter.runtime.TypeRegistryUniversal; import org.example.ebnfFormatter.runtime.TypeSpec; @@ -23,15 +30,34 @@ public PatternMatcher(TypeRegistryUniversal typeRegistry, RuleRegistry ruleRegis this.ruleRegistry = ruleRegistry; } - private boolean matchFork(PatternAst pattern, Object value, Bindings b) { + public MatchResult match(RuleDef rule, Object value) { + AppliedRule appliedRule = matchRuleApplication(rule.name(), rule, value); + if (appliedRule == null) { + return MatchResult.failure(); + } + return MatchResult.success(appliedRule); + } + + private AppliedRule matchRuleApplication(String logicalName, RuleDef rule, Object value) { + Bindings bindings = new Bindings(); + if (!matchFork(rule.pattern(), value, bindings)) { + return null; + } + + Bindings scopedBindings = bindings.copy(); + attachSelfBindings(scopedBindings, logicalName, rule.pattern(), value); + return new AppliedRule(logicalName, rule, value, scopedBindings); + } + + private boolean matchFork(PatternAst pattern, Object value, Bindings bindings) { return switch (pattern) { case Lit lit -> matchLit(lit, value); - case RuleRef ref -> matchRuleRef(ref, value, b); - case NodePat nodePat -> matchNodePat(nodePat, value, b); - case Seq seq -> matchSeq(seq, value, b); - case Alt alt -> matchAlt(alt, value, b); - case ListPat listPat -> matchListPat(listPat, value, b); - case Quant quant -> matchQuant(quant, value, b); + case RuleRef ref -> matchRuleRef(ref, value, bindings); + case NodePat nodePat -> matchNodePat(nodePat, value, bindings); + case Seq seq -> matchSeq(seq, value, bindings); + case Alt alt -> matchAlt(alt, value, bindings); + case ListPat listPat -> matchListPat(listPat, value, bindings); + case Quant quant -> matchQuant(quant, value, bindings); default -> false; }; } @@ -45,20 +71,16 @@ private boolean matchRuleRef(RuleRef ref, Object value, Bindings bindings) { if (!rules.isEmpty()) { for (RuleDef rule : rules) { - Bindings copy = bindings.copy(); - if (matchFork(rule.pattern(), value, copy)) { - if (!copy.bind(ref.name(), value)) { - return false; - } - bindings.replaceWith(copy); - return true; + AppliedRule appliedRule = matchRuleApplication(ref.name(), rule, value); + if (appliedRule != null) { + return bindings.bind(ref.name(), new AppliedRuleValue(appliedRule)); } } return false; } if (value == null) { - return bindings.bind(ref.name(), null); + return bindings.bind(ref.name(), new RawValue(null)); } try { @@ -67,13 +89,13 @@ private boolean matchRuleRef(RuleRef ref, Object value, Bindings bindings) { return false; } } catch (IllegalArgumentException e) { - return bindings.bind(ref.name(), value); + return bindRawValue(ref.name(), value, bindings); } - return bindings.bind(ref.name(), value); + return bindRawValue(ref.name(), value, bindings); } - private boolean matchNodePat(NodePat pat, Object value, Bindings b) { + private boolean matchNodePat(NodePat pat, Object value, Bindings bindings) { if (!(value instanceof Node node)) { return false; } @@ -83,7 +105,7 @@ private boolean matchNodePat(NodePat pat, Object value, Bindings b) { return false; } - Bindings copy = b.copy(); + Bindings copy = bindings.copy(); for (FieldPat field : pat.fields()) { Object fieldValue = typeRegistry.readProperty(node, field.name()); @@ -97,38 +119,36 @@ private boolean matchNodePat(NodePat pat, Object value, Bindings b) { } } - b.replaceWith(copy); + bindings.replaceWith(copy); return true; } - private boolean matchSeq(Seq seq, Object value, Bindings b) { + private boolean matchSeq(Seq seq, Object value, Bindings bindings) { List values = toList(value); - return matchSequence(seq.items(), values, 0, 0, b) == values.size(); + return matchSequence(seq.items(), values, 0, 0, bindings) == values.size(); } - private int matchSequence(List pats, List values, int pi, int vi, Bindings bindings) { - if (pi == pats.size()) { - return vi; + private int matchSequence(List patterns, List values, int patternIndex, int valueIndex, Bindings bindings) { + if (patternIndex == patterns.size()) { + return valueIndex; } - PatternAst currentPattern = pats.get(pi); + PatternAst currentPattern = patterns.get(patternIndex); if (currentPattern instanceof Quant quant) { - return matchQuantified(pats, values, pi, vi, quant, bindings); + return matchQuantified(patterns, values, patternIndex, valueIndex, quant, bindings); } - if (vi >= values.size()) { + if (valueIndex >= values.size()) { return -1; } Bindings trial = bindings.copy(); - boolean matched = matchFork(currentPattern, values.get(vi), trial); - - if (!matched) { + if (!matchFork(currentPattern, values.get(valueIndex), trial)) { return -1; } - int nextIndex = matchSequence(pats, values, pi + 1, vi + 1, trial); + int nextIndex = matchSequence(patterns, values, patternIndex + 1, valueIndex + 1, trial); if (nextIndex == -1) { return -1; } @@ -138,46 +158,47 @@ private int matchSequence(List pats, List values, int pi, int vi, } private int matchQuantified( - List pats, + List patterns, List values, - int pi, - int vi, + int patternIndex, + int valueIndex, Quant quant, Bindings bindings ) { - PatternAst inner = quant.pattern(); - return switch (quant.quantifier()) { - case OPTIONAL -> matchOptional(pats, values, pi, vi, inner, bindings); - case ZERO_OR_MORE -> matchZeroOrMore(pats, values, pi, vi, inner, bindings); - case ONE_OR_MORE -> matchOneOrMore(pats, values, pi, vi, inner, bindings); + case OPTIONAL -> matchOptional(patterns, values, patternIndex, valueIndex, quant.pattern(), bindings); + case ZERO_OR_MORE -> matchZeroOrMore(patterns, values, patternIndex, valueIndex, quant.pattern(), bindings); + case ONE_OR_MORE -> matchOneOrMore(patterns, values, patternIndex, valueIndex, quant.pattern(), bindings); }; } private int matchOptional( - List pats, + List patterns, List values, - int pi, - int vi, + int patternIndex, + int valueIndex, PatternAst inner, - Bindings bindings) { + Bindings bindings + ) { Bindings skipBindings = bindings.copy(); - int skipResult = matchSequence(pats, values, pi + 1, vi, skipBindings); + int skipResult = matchSequence(patterns, values, patternIndex + 1, valueIndex, skipBindings); if (skipResult != -1) { bindings.replaceWith(skipBindings); return skipResult; } - if (vi >= values.size()) { + if (valueIndex >= values.size()) { return -1; } Bindings takeBindings = bindings.copy(); - if (!matchFork(inner, values.get(vi), takeBindings)) { + Bindings iterationBindings = matchQuantifiedIteration(inner, values.get(valueIndex)); + if (iterationBindings == null) { return -1; } + takeBindings.appendAll(iterationBindings); - int takeResult = matchSequence(pats, values, pi + 1, vi + 1, takeBindings); + int takeResult = matchSequence(patterns, values, patternIndex + 1, valueIndex + 1, takeBindings); if (takeResult == -1) { return -1; } @@ -187,29 +208,30 @@ private int matchOptional( } private int matchZeroOrMore( - List pats, + List patterns, List values, - int pi, - int vi, + int patternIndex, + int valueIndex, PatternAst inner, - Bindings bindings) { + Bindings bindings + ) { List snapshots = new ArrayList<>(); List indexes = new ArrayList<>(); Bindings currentBindings = bindings.copy(); - int currentIndex = vi; + int currentIndex = valueIndex; snapshots.add(currentBindings.copy()); indexes.add(currentIndex); while (currentIndex < values.size()) { - Bindings nextBindings = currentBindings.copy(); - if (!matchFork(inner, values.get(currentIndex), nextBindings)) { + Bindings iterationBindings = matchQuantifiedIteration(inner, values.get(currentIndex)); + if (iterationBindings == null) { break; } + currentBindings.appendAll(iterationBindings); currentIndex++; - currentBindings = nextBindings; snapshots.add(currentBindings.copy()); indexes.add(currentIndex); @@ -219,7 +241,7 @@ private int matchZeroOrMore( Bindings candidate = snapshots.get(i).copy(); int candidateIndex = indexes.get(i); - int result = matchSequence(pats, values, pi + 1, candidateIndex, candidate); + int result = matchSequence(patterns, values, patternIndex + 1, candidateIndex, candidate); if (result != -1) { bindings.replaceWith(candidate); return result; @@ -230,30 +252,37 @@ private int matchZeroOrMore( } private int matchOneOrMore( - List pats, + List patterns, List values, - int pi, - int vi, + int patternIndex, + int valueIndex, PatternAst inner, Bindings bindings ) { - if (vi >= values.size()) { + if (valueIndex >= values.size()) { return -1; } - Bindings firstBindings = bindings.copy(); - if (!matchFork(inner, values.get(vi), firstBindings)) { + Bindings firstIteration = matchQuantifiedIteration(inner, values.get(valueIndex)); + if (firstIteration == null) { return -1; } - return matchZeroOrMoreAfterFirst(pats, values, pi, vi + 1, inner, firstBindings); + Bindings firstBindings = bindings.copy(); + firstBindings.appendAll(firstIteration); + + int result = matchZeroOrMoreAfterFirst(patterns, values, patternIndex, valueIndex + 1, inner, firstBindings); + if (result != -1) { + bindings.replaceWith(firstBindings); + } + return result; } private int matchZeroOrMoreAfterFirst( - List pats, + List patterns, List values, - int pi, - int vi, + int patternIndex, + int valueIndex, PatternAst inner, Bindings bindings ) { @@ -261,19 +290,19 @@ private int matchZeroOrMoreAfterFirst( List indexes = new ArrayList<>(); Bindings currentBindings = bindings.copy(); - int currentIndex = vi; + int currentIndex = valueIndex; snapshots.add(currentBindings.copy()); indexes.add(currentIndex); while (currentIndex < values.size()) { - Bindings nextBindings = currentBindings.copy(); - if (!matchFork(inner, values.get(currentIndex), nextBindings)) { + Bindings iterationBindings = matchQuantifiedIteration(inner, values.get(currentIndex)); + if (iterationBindings == null) { break; } + currentBindings.appendAll(iterationBindings); currentIndex++; - currentBindings = nextBindings; snapshots.add(currentBindings.copy()); indexes.add(currentIndex); @@ -283,7 +312,7 @@ private int matchZeroOrMoreAfterFirst( Bindings candidate = snapshots.get(i).copy(); int candidateIndex = indexes.get(i); - int result = matchSequence(pats, values, pi + 1, candidateIndex, candidate); + int result = matchSequence(patterns, values, patternIndex + 1, candidateIndex, candidate); if (result != -1) { bindings.replaceWith(candidate); return result; @@ -293,33 +322,37 @@ private int matchZeroOrMoreAfterFirst( return -1; } - private boolean matchAlt(Alt pat, Object value, Bindings b) { + private boolean matchAlt(Alt pat, Object value, Bindings bindings) { for (PatternAst option : pat.options()) { - Bindings copy = b.copy(); + Bindings copy = bindings.copy(); if (matchFork(option, value, copy)) { - b.replaceWith(copy); + bindings.replaceWith(copy); return true; } } return false; } - private boolean matchListPat(ListPat pat, Object value, Bindings b) { + private boolean matchListPat(ListPat pat, Object value, Bindings bindings) { List values = toList(value); - return matchSequence(pat.items(), values, 0, 0, b) == values.size(); + return matchSequence(pat.items(), values, 0, 0, bindings) == values.size(); } - private boolean matchQuant(Quant quant, Object value, Bindings b) { + private boolean matchQuant(Quant quant, Object value, Bindings bindings) { return switch (quant.quantifier()) { case OPTIONAL -> { if (value == null) { yield true; } - Bindings copy = b.copy(); - if (!matchFork(quant.pattern(), value, copy)) { + + Bindings iterationBindings = matchQuantifiedIteration(quant.pattern(), value); + if (iterationBindings == null) { yield false; } - b.replaceWith(copy); + + Bindings copy = bindings.copy(); + copy.appendAll(iterationBindings); + bindings.replaceWith(copy); yield true; } case ZERO_OR_MORE -> { @@ -327,22 +360,16 @@ private boolean matchQuant(Quant quant, Object value, Bindings b) { yield true; } - List values = toList(value); - Bindings copy = b.copy(); - - boolean ok = true; - for (Object item : values) { - if (!matchFork(quant.pattern(), item, copy)) { - ok = false; - break; + Bindings copy = bindings.copy(); + for (Object item : toList(value)) { + Bindings iterationBindings = matchQuantifiedIteration(quant.pattern(), item); + if (iterationBindings == null) { + yield false; } + copy.appendAll(iterationBindings); } - if (!ok) { - yield false; - } - - b.replaceWith(copy); + bindings.replaceWith(copy); yield true; } case ONE_OR_MORE -> { @@ -355,26 +382,66 @@ private boolean matchQuant(Quant quant, Object value, Bindings b) { yield false; } - Bindings copy = b.copy(); - - boolean ok = true; + Bindings copy = bindings.copy(); for (Object item : values) { - if (!matchFork(quant.pattern(), item, copy)) { - ok = false; - break; + Bindings iterationBindings = matchQuantifiedIteration(quant.pattern(), item); + if (iterationBindings == null) { + yield false; } + copy.appendAll(iterationBindings); } - if (!ok) { - yield false; - } - - b.replaceWith(copy); + bindings.replaceWith(copy); yield true; } }; } + private Bindings matchQuantifiedIteration(PatternAst inner, Object value) { + Bindings iterationBindings = new Bindings(); + if (!matchFork(inner, value, iterationBindings)) { + return null; + } + return iterationBindings; + } + + private void attachSelfBindings(Bindings bindings, String logicalName, PatternAst pattern, Object value) { + if (pattern instanceof NodePat nodePat) { + bindIfAbsent(bindings, nodePat.typeName(), new RawValue(value)); + } + } + + private void bindIfAbsent(Bindings bindings, String name, BoundValue value) { + if (!bindings.hasBinding(name)) { + bindings.bind(name, value); + } + } + + private boolean bindRawValue(String name, Object value, Bindings bindings) { + if (value == null) { + return bindings.bind(name, new RawValue(null)); + } + + if (value instanceof Iterable iterable) { + List items = new ArrayList<>(); + for (Object item : iterable) { + items.add(new RawValue(item)); + } + return bindings.bindAll(name, items); + } + + if (value.getClass().isArray()) { + int length = Array.getLength(value); + List items = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + items.add(new RawValue(Array.get(value, i))); + } + return bindings.bindAll(name, items); + } + + return bindings.bind(name, new RawValue(value)); + } + private List toList(Object value) { if (value == null) { return List.of(); @@ -395,15 +462,4 @@ private List toList(Object value) { return List.of(value); } - - public MatchResult match(PatternAst pattern, Node node) { - Bindings bindings = new Bindings(); - boolean matched = matchFork(pattern, node, bindings); - - if (!matched) { - return MatchResult.failure(); - } - - return MatchResult.success(bindings); - } -} \ No newline at end of file +} diff --git a/src/main/java/org/example/ebnfFormatter/render/NestedRuleRenderer.java b/src/main/java/org/example/ebnfFormatter/render/NestedRuleRenderer.java new file mode 100644 index 0000000..08d532d --- /dev/null +++ b/src/main/java/org/example/ebnfFormatter/render/NestedRuleRenderer.java @@ -0,0 +1,8 @@ +package org.example.ebnfFormatter.render; + +import java.util.Optional; + +@FunctionalInterface +public interface NestedRuleRenderer { + Optional tryRender(String ruleName, Object value); +} diff --git a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java index 8e2ba38..aaaf2fb 100644 --- a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java +++ b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java @@ -1,12 +1,13 @@ package org.example.ebnfFormatter.render; import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.Expression; -import com.github.javaparser.ast.stmt.BlockStmt; -import com.github.javaparser.ast.stmt.ForStmt; -import com.github.javaparser.ast.stmt.IfStmt; -import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.stmt.*; +import org.example.ebnfFormatter.match.AppliedRuleValue; import org.example.ebnfFormatter.match.Bindings; +import org.example.ebnfFormatter.match.BoundValue; +import org.example.ebnfFormatter.match.RawValue; import org.example.ebnfFormatter.model.format.*; import java.lang.reflect.Array; @@ -18,38 +19,44 @@ public final class TemplateRenderer { public String render(FormatAst format, Bindings bindings) { + return render(format, bindings, (ruleName, value) -> Optional.empty()); + } + + public String render(FormatAst format, Bindings bindings, NestedRuleRenderer nestedRuleRenderer) { RenderContext context = new RenderContext(); - renderInto(format, bindings, context); + renderInto(format, bindings, nestedRuleRenderer, context); return context.result(); } - private void renderInto(FormatAst format, Bindings bindings, RenderContext context) { + private void renderInto( + FormatAst format, + Bindings bindings, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context + ) { switch (format) { case FormatText text -> context.appendText(text.text()); - case FormatPlaceholder placeholder -> { - Object value = bindings.getRequired(placeholder.name()); - renderValue(value, context); - } + case FormatPlaceholder placeholder -> + renderPlaceholder(placeholder.name(), bindings, nestedRuleRenderer, context); case FormatDirective directive -> renderDirective(directive, context); case FormatSeq seq -> { for (FormatAst item : seq.items()) { - renderInto(item, bindings, context); + renderInto(item, bindings, nestedRuleRenderer, context); } } - case FormatGroup group -> renderInto(group.body(), bindings, context); + case FormatGroup group -> renderInto(group.body(), bindings, nestedRuleRenderer, context); case FormatIfPresent ifPresent -> { - Object value = bindings.find(ifPresent.name()); - if (isPresent(value)) { - renderInto(ifPresent.body(), bindings, context); + if (!bindings.findValues(ifPresent.name()).isEmpty()) { + renderInto(ifPresent.body(), bindings, nestedRuleRenderer, context); } } - case FormatJoin join -> renderJoin(join, bindings, context); + case FormatJoin join -> renderJoin(join, bindings, nestedRuleRenderer, context); } } @@ -62,25 +69,70 @@ private void renderDirective(FormatDirective directive, RenderContext context) { } } - private void renderJoin(FormatJoin join, Bindings bindings, RenderContext context) { - Object raw = bindings.find(join.placeholderName()); - List items = toList(raw); + private void renderPlaceholder( + String placeholderName, + Bindings bindings, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context + ) { + List values = bindings.getRequiredValues(placeholderName); + for (BoundValue value : values) { + renderBoundValue(placeholderName, value, nestedRuleRenderer, context); + } + } + + private void renderJoin( + FormatJoin join, + Bindings bindings, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context + ) { + List items = bindings.findValues(join.placeholderName()); for (int i = 0; i < items.size(); i++) { if (i > 0) { - renderInto(join.separator(), bindings, context); + renderInto(join.separator(), bindings, nestedRuleRenderer, context); } - renderValue(items.get(i), context); + renderBoundValue(join.placeholderName(), items.get(i), nestedRuleRenderer, context); } } - private void renderValue(Object value, RenderContext context) { + private void renderBoundValue( + String placeholderName, + BoundValue boundValue, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context + ) { + switch (boundValue) { + case AppliedRuleValue appliedRuleValue -> + renderInto( + appliedRuleValue.appliedRule().rule().format(), + appliedRuleValue.appliedRule().bindings(), + nestedRuleRenderer, + context + ); + case RawValue rawValue -> renderRawValue(placeholderName, rawValue.value(), nestedRuleRenderer, context); + } + } + + private void renderRawValue( + String placeholderName, + Object value, + NestedRuleRenderer nestedRuleRenderer, + RenderContext context + ) { if (value == null) { return; } + Optional nested = nestedRuleRenderer.tryRender(stripQuantifierSuffix(placeholderName), value); + if (nested.isPresent()) { + context.appendText(nested.get()); + return; + } + if (value instanceof Optional optional) { - optional.ifPresent(v -> renderValue(v, context)); + optional.ifPresent(v -> renderRawValue(placeholderName, v, nestedRuleRenderer, context)); return; } @@ -93,7 +145,7 @@ private void renderValue(Object value, RenderContext context) { Iterator it = iterable.iterator(); while (it.hasNext()) { Object item = it.next(); - renderValue(item, context); + renderRawValue(placeholderName, item, nestedRuleRenderer, context); } return; } @@ -102,7 +154,7 @@ private void renderValue(Object value, RenderContext context) { if (type.isArray()) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { - renderValue(Array.get(value, i), context); + renderRawValue(placeholderName, Array.get(value, i), nestedRuleRenderer, context); } return; } @@ -126,9 +178,70 @@ private void renderNode(Node node, RenderContext context) { return; } + if (node instanceof MethodDeclaration methodDeclaration) { + renderMethodDeclaration(methodDeclaration, context); + return; + } + + if (node instanceof ReturnStmt returnStmt) { + renderReturnStmt(returnStmt, context); + return; + } + + if (node instanceof ExpressionStmt expressionStmt) { + renderExpressionStmt(expressionStmt, context); + return; + } + + context.appendText(node.toString()); } + private void renderReturnStmt(com.github.javaparser.ast.stmt.ReturnStmt stmt, RenderContext context) { + context.appendText("return"); + context.space(); + context.appendText(stmt.getExpression().map(Node::toString).orElse("")); + context.appendText(";"); + } + + private void renderExpressionStmt(com.github.javaparser.ast.stmt.ExpressionStmt stmt, RenderContext context) { + context.appendText(stmt.getExpression().toString()); + context.appendText(";"); + } + + private void renderMethodDeclaration(MethodDeclaration method, RenderContext context) { + if (!method.getModifiers().isEmpty()) { + for (int i = 0; i < method.getModifiers().size(); i++) { + if (i > 0) { + context.space(); + } + context.appendText(method.getModifiers().get(i).getKeyword().asString()); + } + context.space(); + } + + context.appendText(method.getType().toString()); + context.space(); + context.appendText(method.getNameAsString()); + context.appendText("("); + + for (int i = 0; i < method.getParameters().size(); i++) { + if (i > 0) { + context.appendText(", "); + } + context.appendText(method.getParameter(i).toString()); + } + + context.appendText(")"); + + if (method.getBody().isPresent()) { + context.space(); + renderBlockStmt(method.getBody().get(), context); + } else { + context.appendText(";"); + } + } + private void renderBlockStmt(BlockStmt block, RenderContext context) { context.appendText("{"); context.newline(); @@ -247,6 +360,18 @@ private boolean isPresent(Object value) { return true; } + private String stripQuantifierSuffix(String name) { + if (name == null || name.isEmpty()) { + return name; + } + + char last = name.charAt(name.length() - 1); + return switch (last) { + case '?', '*', '+' -> name.substring(0, name.length() - 1); + default -> name; + }; + } + private List toList(Object value) { if (value == null) { return List.of(); @@ -276,4 +401,4 @@ private List toList(Object value) { return List.of(value); } -} \ No newline at end of file +} diff --git a/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java b/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java index e0e3ccf..06cd440 100644 --- a/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java +++ b/src/main/java/org/example/ebnfFormatter/runtime/FormatterEngine.java @@ -6,6 +6,8 @@ import org.example.ebnfFormatter.model.RuleDef; import org.example.ebnfFormatter.render.TemplateRenderer; +import java.util.Optional; + public final class FormatterEngine { private final RuleRegistry ruleRegistry; private final PatternMatcher patternMatcher; @@ -22,14 +24,30 @@ public FormatterEngine( } public String format(Node node, String ruleName) { + return tryRender(ruleName, node) + .orElseThrow(() -> new IllegalArgumentException( + "Node does not match any rule <" + ruleName + ">" + )); + } + + public Optional tryRender(String ruleName, Object value) { for (RuleDef rule : ruleRegistry.requireAll(ruleName)) { - MatchResult match = patternMatcher.match(rule.pattern(), node); + MatchResult match = patternMatcher.match(rule, value); if (match.matched()) { - return templateRenderer.render(rule.format(), match.bindings()); + return Optional.of(templateRenderer.render( + rule.format(), + match.bindings(), + this::tryRenderNested + )); } } - throw new IllegalArgumentException( - "Node does not match any rule <" + ruleName + ">" - ); + return Optional.empty(); + } + + private Optional tryRenderNested(String ruleName, Object value) { + if (!ruleRegistry.contains(ruleName)) { + return Optional.empty(); + } + return tryRender(ruleName, value); } -} \ No newline at end of file +} From 04d382405cd3e82d6f36b260eed5603de808a68d Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Apr 2026 16:14:02 +0300 Subject: [PATCH 15/29] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B,=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D1=8C=20=D1=82=D0=B0=D0=BC=20=D0=BD=D0=B5?= =?UTF-8?q?=D1=82=20=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4=D0=B0,=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=B8=D1=80=D1=83=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE=20=D0=BE=D0=B1=D1=89=D0=B8?= =?UTF-8?q?=D0=BC=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExamplesFromDocumentationTest.java | 205 +++++++----------- 1 file changed, 77 insertions(+), 128 deletions(-) diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java index b2e455f..9615c1f 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java @@ -3,6 +3,7 @@ import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.stmt.BlockStmt; import com.github.javaparser.ast.stmt.ForStmt; import com.github.javaparser.ast.stmt.IfStmt; import org.antlr.v4.runtime.CharStreams; @@ -21,9 +22,30 @@ public class ExamplesFromDocumentationTest { private static final String ifStmtRules = """ - ::= IfStmt(condition=, thenStmt=, elseStmt?=) - => "if" sp "(" ")" sp - ifpresent(Statement?, sp "else" sp ); + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= IfStmt + => sp ; + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; """; @Test @@ -48,21 +70,6 @@ public int sum(int a, int b) { @Test void ifStmtExample2() { - String rules = """ - ::= IfStmt(condition=, thenStmt=, elseStmt=) - => "if" sp "(" ")" nl indent dedent - nl "else" sp nl indent dedent; - - ::= ReturnStmt(expression=) - => nl indent "return" sp ";" dedent; - - ::= ReturnStmt(expression=) - => nl indent "return" sp ";" dedent; - - ::= IfStmt - => ; - """; - String code = """ public class AST { public int sum(int a, int b) { @@ -75,30 +82,15 @@ public int sum(int a, int b) { String expected = """ if (a == b) return 2 * a; - else\s + else return a + b;"""; - String formatted = formatFirstNode(rules, code, IfStmt.class, "IfStmt"); + String formatted = formatFirstNode(ifStmtRules, code, IfStmt.class, "IfStmt"); assertThat(formatted).isEqualTo(expected); } @Test void ifStmtExample3() { - String rules = """ - ::= IfStmt(condition=, thenStmt=, elseStmt=) - => "if" sp "(" ")" nl indent dedent - nl "else" sp nl indent dedent; - - ::= ReturnStmt(expression=) - => nl indent "return" sp ";" dedent; - - ::= ReturnStmt(expression=) - => nl indent "return" sp ";" dedent; - - ::= IfStmt - => ; - """; - String code = """ public class AST { public int sum(int a, int b) { @@ -112,36 +104,29 @@ public int sum(int a, int b) { String expected = """ if (a == b) return 2 * a; - else\s - if ((a & 2) == 2) - return a + b; - else - return b;"""; + else if ((a & 2) == 2) + return a + b; + else + return b;"""; - String formatted = formatFirstNode(rules, code, IfStmt.class, "IfStmt"); + String formatted = formatFirstNode(ifStmtRules, code, IfStmt.class, "IfStmt"); assertThat(formatted).isEqualTo(expected); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// private static final String methodDeclarationRules = """ -// ::= MethodDeclaration( -// modifiers=[], -// type=, -// name=, -// parameters=[], -// body?=) -// => join(, sp) sp sp -// "(" ifpresent(Parameter*, join(, ", ")) ")" -// ifpresent(Statement?, sp ); -// """; + private static final String methodDeclarationRules = """ + ::= MethodDeclaration( + modifiers=[*], + type=, + name=, + parameters=[*], + body?=) + => join(, "") sp + "(" ifpresent(Parameter*, join(, ", ")) ")" + ifpresent(Statement?, sp ); + """; @Test void methodDeclarationExample1() { - String rules = """ - ::= MethodDeclaration(body=BlockStmt) - => "public" sp "int" sp "sum" "(" "int a, int b" ")" sp "{" - nl indent "if(a==b){return 2*a;}return a+b;" nl dedent "}"; - """; - String code = """ public class AST { public int sum(int a,int b){if(a==b){return 2*a;}return a+b;} @@ -150,22 +135,18 @@ public class AST { String expected = """ public int sum(int a, int b) { - if(a==b){return 2*a;}return a+b; + if (a == b) { + return 2 * a; + } + return a + b; }"""; - String formatted = formatFirstNode(rules, code, MethodDeclaration.class, "MethodDeclaration"); + String formatted = formatFirstNode(methodDeclarationRules, code, MethodDeclaration.class, "MethodDeclaration"); assertThat(formatted).isEqualTo(expected); } @Test void methodDeclarationExample2() { - String rules = """ - ::= MethodDeclaration(body=BlockStmt) - => "public" sp "int" sp "sum" "(" - "Parameter a, Parameter b, Parameter c, Parameter d" - ")" sp "{" nl indent nl dedent "}"; - """; - String code = """ public class AST { public int sum(Parameter a,Parameter b,Parameter c,Parameter d) {} @@ -177,18 +158,12 @@ public int sum(Parameter a, Parameter b, Parameter c, Parameter d) { }"""; - String formatted = formatFirstNode(rules, code, MethodDeclaration.class, "MethodDeclaration"); + String formatted = formatFirstNode(methodDeclarationRules, code, MethodDeclaration.class, "MethodDeclaration"); assertThat(formatted).isEqualTo(expected); } @Test void methodDeclarationExample3() { - String rules = """ - ::= MethodDeclaration - => "public" sp "abstract" sp "int" sp "sum" "(" "Input input" ")" ";"; - """; - - String code = """ abstract class AST { public abstract int sum(Input @@ -196,43 +171,36 @@ public abstract int sum(Input } """; - String expected = "public abstract int sum(Input input);"; + String expected = "public abstract int sum(Input input)"; - String formatted = formatFirstNode(rules, code, MethodDeclaration.class, "MethodDeclaration"); + String formatted = formatFirstNode(methodDeclarationRules, code, MethodDeclaration.class, "MethodDeclaration"); assertThat(formatted).isEqualTo(expected); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// private static final String forStmtRules = """ -// ::= ForStmt( -// initialization=[], -// compare?=, -// update=[], -// body= -// ) -// => "for" sp "(" -// ifpresent(Expression*, join(, ", ")) -// ";" ifpresent(Expression?, sp ) -// ";" ifpresent(Expression*, sp join(, ", ")) -// ")" sp ; -// -// ::= BlockStmt(statements=[]) -// => "{" nl indent join(, nl) nl dedent "}"; -// -// ::= ExpressionStmt(expression=) -// => nl indent ";" dedent; -// -// ::= ReturnStmt(expression=) -// => nl indent "return" sp ";" dedent; -// """; + private static final String forStmtRules = """ + ::= ForStmt( + initialization=[*], + compare?=, + update=[*], + body=) + => "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" ; + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + """; @Test void forStmtExample1() { - String rules = """ - ::= ForStmt(body=BlockStmt) - => "for" sp "(" "int i=0" ";" sp "i<5" ";" sp "++i" ")" sp "{" - nl indent "sm += i;" nl dedent "}"; - """; - String code = """ public class AST { public int sum(int a, int b) { @@ -243,21 +211,16 @@ public int sum(int a, int b) { """; String expected = """ - for (int i=0; i<5; ++i) { + for (int i = 0; i < 5; ++i) { sm += i; }"""; - String formatted = formatFirstNode(rules, code, ForStmt.class, "ForStmt"); + String formatted = formatFirstNode(forStmtRules, code, ForStmt.class, "ForStmt"); assertThat(formatted).isEqualTo(expected); } @Test void forStmtExample2() { - String rules = """ - ::= ForStmt(body=ExpressionStmt) - => "for" sp "(" "int i=0" ";" sp "i<5" ";" sp "++i" ")" nl indent "sm += i;" dedent; - """; - String code = """ public class AST { public int sum(int a, int b) { @@ -268,29 +231,15 @@ public int sum(int a, int b) { """; String expected = """ - for (int i=0; i<5; ++i) + for (int i = 0; i < 5; ++i) sm += i;"""; - String formatted = formatFirstNode(rules, code, ForStmt.class, "ForStmt"); + String formatted = formatFirstNode(forStmtRules, code, ForStmt.class, "ForStmt"); assertThat(formatted).isEqualTo(expected); } @Test void forStmtExample3() { - String rules = """ - ::= ForStmt(body=) - => "for" sp "(" ";" ";" ")" nl indent dedent; - - ::= ExpressionStmt(expression=) - => ";"; - - ::= MethodCallExpr(name=) - => "(" ")"; - - ::= SimpleName(identifier="make") - => "make"; - """; - String code = """ public class AST { public int sum(int a, int b) { @@ -303,7 +252,7 @@ public int sum(int a, int b) { for (;;) make();"""; - String formatted = formatFirstNode(rules, code, ForStmt.class, "ForStmt"); + String formatted = formatFirstNode(forStmtRules, code, ForStmt.class, "ForStmt"); assertThat(formatted).isEqualTo(expected); } From ff0dc064693e07d176e979b8a8ef86ce06d0869c Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 15 Apr 2026 16:16:50 +0300 Subject: [PATCH 16/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D0=BD=D0=B0=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BE=D1=82=20CompilationUnit=20=D0=B4=D0=BE=20=D0=BB=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D1=8C=D0=B5=D0=B2=20AST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/ALotOfEndToEndTest.java | 742 ++++++++++++++++++ 1 file changed, 742 insertions(+) create mode 100644 src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java new file mode 100644 index 0000000..1ef2f52 --- /dev/null +++ b/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java @@ -0,0 +1,742 @@ +package org.example.ebnfFormatter.runtime; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.example.ebnfFormatter.dsl.RuleAstBuilder; +import org.example.ebnfFormatter.match.PatternMatcher; +import org.example.ebnfFormatter.model.RuleDef; +import org.example.ebnfFormatter.render.TemplateRenderer; +import org.example.ebnfLexer; +import org.example.ebnfParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ALotOfEndToEndTest { + + private static final String ALL_RULES = """ + ::= CompilationUnit(packageDeclaration?=, imports=[*], types=[*]) + => ifpresent(PackageDeclaration, nl nl) + ifpresent(ImportDeclaration, join(, nl) nl nl) + join(, nl nl); + + ::= PackageDeclaration(name=) + => "package" sp ";"; + + ::= ImportDeclaration(name=) + => "import" sp ";"; + + ::= ClassOrInterfaceDeclaration(modifiers=[*], name=, members=[*]) + => ifpresent(Modifier, join(, "")) "class" sp sp "{" + ifpresent(MethodDeclaration, nl indent join(, nl nl) nl dedent) + "}"; + + ::= MethodDeclaration(modifiers=[*], type=, name=, parameters=[*], body=) + => ifpresent(Modifier, join(, "")) sp "(" + ifpresent(Parameter, join(, ", ")) + ")" sp ; + + ::= MethodDeclaration(modifiers=[*], type=, name=, parameters=[*]) + => ifpresent(Modifier, join(, "")) sp "(" + ifpresent(Parameter, join(, ", ")) + ")" ";"; + + ::= BlockStmt(statements=[*]) + => "{" nl indent join(, nl) nl dedent "}"; + + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => nl indent "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" dedent; + + ::= + => sp ; + + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => nl indent "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" ; + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ReturnStmt(expression=) + => "return" sp ";"; + + ::= ExpressionStmt(expression=) + => ";"; + """; + private static final List ruleDefs = parseRules(); + private static final RuleRegistry registry = registryWithRules(); + + private static RuleRegistry registryWithRules() { + RuleRegistry ruleRegistry = new RuleRegistry(); + ruleRegistry.registerAll(ruleDefs); + return ruleRegistry; + } + + private static final TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); + private static final PatternMatcher matcher = new PatternMatcher(typeRegistry, registry); + private static final FormatterEngine engine = new FormatterEngine(registry, matcher, new TemplateRenderer()); + + @Test + void formats_whole_file_with_single_class_and_single_method() { + assertFormatsWholeFile( + """ + public class Sample{public int one(){return 1;}} + """, + """ + public class Sample { + public int one() { + return 1; + } + }""" + ); + } + + @Test + void formats_whole_file_with_public_final_empty_class() { + assertFormatsWholeFile( + """ + public final class Empty{} + """, + """ + public final class Empty {}""" + ); + } + + @Test + void formats_whole_file_with_empty_method_body() { + assertFormatsWholeFile( + """ + class EmptyMethod{void run(){}} + """, + """ + class EmptyMethod { + void run() { + + } + }""" + ); + } + + @Test + void formats_whole_file_with_abstract_method() { + assertFormatsWholeFile( + """ + abstract class AbstractMath{public abstract int sum(int a,int b);} + """, + """ + abstract class AbstractMath { + public abstract int sum(int a, int b); + }""" + ); + } + + @Test + void formats_whole_file_with_static_method_and_parameters() { + assertFormatsWholeFile( + """ + public class MathBox{public static int sum(int a,int b){return a+b;}} + """, + """ + public class MathBox { + public static int sum(int a, int b) { + return a + b; + } + }""" + ); + } + + @Test + void formats_whole_file_with_two_methods() { + assertFormatsWholeFile( + """ + class PairOps{int left(){return 1;}int right(){return 2;}} + """, + """ + class PairOps { + int left() { + return 1; + } + + int right() { + return 2; + } + }""" + ); + } + + @Test + void formats_whole_file_with_if_else_returns() { + assertFormatsWholeFile( + """ + class Branches{int max(int a,int b){if(a>b)return a;else return b;}} + """, + """ + class Branches { + int max(int a, int b) { + if (a > b) + return a; + else + return b; + } + }""" + ); + } + + @Test + void formats_whole_file_with_else_if_chain() { + assertFormatsWholeFile( + """ + class Branches{int choose(int a,int b){if(a>b)return a;else if(a==b)return 0;else return b;}} + """, + """ + class Branches { + int choose(int a, int b) { + if (a > b) + return a; + else if (a == b) + return 0; + else + return b; + } + }""" + ); + } + + @Test + void formats_whole_file_with_if_block_and_else_block() { + assertFormatsWholeFile( + """ + class Branches{int max(int a,int b){if(a>b){a++;return a;}else{b++;return b;}}} + """, + """ + class Branches { + int max(int a, int b) { + if (a > b) { + a++; + return a; + } + else { + b++; + return b; + } + } + }""" + ); + } + + @Test + void formats_whole_file_with_if_without_else_and_expression_body() { + assertFormatsWholeFile( + """ + class Steps{void run(boolean ready){if(ready)step();}} + """, + """ + class Steps { + void run(boolean ready) { + if (ready) + step(); + } + }""" + ); + } + + @Test + void formats_whole_file_with_for_compare_and_expression_body() { + assertFormatsWholeFile( + """ + class Loop{void run(){for(i=0,j=1;i<10;i++,j++)step();}} + """, + """ + class Loop { + void run() { + for (i = 0, j = 1; i < 10; i++, j++) + step(); + } + }""" + ); + } + + @Test + void formats_whole_file_with_for_without_compare() { + assertFormatsWholeFile( + """ + class Loop{void run(){for(i=0,j=1;;i++,j++)step();}} + """, + """ + class Loop { + void run() { + for (i = 0, j = 1;; i++, j++) + step(); + } + }""" + ); + } + + @Test + void formats_whole_file_with_for_block_body() { + assertFormatsWholeFile( + """ + class Loop{void run(){for(i=0;i<3;i++){step();step();}}} + """, + """ + class Loop { + void run() { + for (i = 0; i < 3; i++) { + step(); + step(); + } + } + }""" + ); + } + + @Test + void formats_whole_file_with_nested_block_statement() { + assertFormatsWholeFile( + """ + class NestedBlock{int run(){{step();return 1;}}} + """, + """ + class NestedBlock { + int run() { + { + step(); + return 1; + } + } + }""" + ); + } + + @Test + void formats_whole_file_with_variable_increment_and_return() { + assertFormatsWholeFile( + """ + class Counter{int run(){int x=1;x++;return x;}} + """, + """ + class Counter { + int run() { + int x = 1; + x++; + return x; + } + }""" + ); + } + + @Test + void formats_whole_file_with_package_declaration() { + assertFormatsWholeFile( + """ + package demo; + class Single{int one(){return 1;}} + """, + """ + package demo; + + class Single { + int one() { + return 1; + } + }""" + ); + } + + @Test + void formats_whole_file_with_imports() { + assertFormatsWholeFile( + """ + import java.util.List; + import java.util.Map; + class Uses{void run(){step();}} + """, + """ + import java.util.List; + import java.util.Map; + + class Uses { + void run() { + step(); + } + }""" + ); + } + + @Test + void formats_whole_file_with_package_imports_and_two_classes() { + assertFormatsWholeFile( + """ + package demo; + import java.util.List; + class First{} + class Second{int two(){return 2;}} + """, + """ + package demo; + + import java.util.List; + + class First {} + + class Second { + int two() { + return 2; + } + }""" + ); + } + + @Test + void formats_whole_file_with_abstract_and_concrete_methods() { + assertFormatsWholeFile( + """ + abstract class Mixed{abstract int one();int two(){return 2;}} + """, + """ + abstract class Mixed { + abstract int one(); + + int two() { + return 2; + } + }""" + ); + } + + @Test + void formats_whole_file_with_multiple_top_level_classes() { + assertFormatsWholeFile( + """ + class First{void a(){alpha();}}class Second{void b(){beta();}} + """, + """ + class First { + void a() { + alpha(); + } + } + + class Second { + void b() { + beta(); + } + }""" + ); + } + + @Test + void formats_whole_file_with_if_then_for_and_else_return() { + assertFormatsWholeFile( + """ + class Complex{int run(int x){if(x>0)for(i=0;i<3;i++)tick();else return x;}} + """, + """ + class Complex { + int run(int x) { + if (x > 0) + for (i = 0; i < 3; i++) + tick(); + else + return x; + } + }""" + ); + } + + @Test + void formats_whole_file_with_if_then_return_and_else_for_block() { + assertFormatsWholeFile( + """ + class Complex{int run(int x){if(x>0)return x;else for(i=0;i<2;i++){tick();tick();}}} + """, + """ + class Complex { + int run(int x) { + if (x > 0) + return x; + else + for (i = 0; i < 2; i++) { + tick(); + tick(); + } + } + }""" + ); + } + + @Test + void formats_whole_file_with_for_block_containing_if_and_following_expression() { + assertFormatsWholeFile( + """ + class Scanner{int scan(int limit){for(i=0;i0){for(i=0;i 0) { + for (i = 0; i < a; i++) + work(); + return a; + } + else { + a--; + return a; + } + } + }""" + ); + } + + @Test + void formats_whole_file_with_package_imports_and_complex_two_classes() { + assertFormatsWholeFile( + """ + package demo.deep; import java.util.List;import java.util.Map;abstract class Base{abstract int value();int fallback(){return 0;}} + class Derived{int run(int a){if(a>1)return a;else return 1;}} + """, + """ + package demo.deep; + + import java.util.List; + import java.util.Map; + + abstract class Base { + abstract int value(); + + int fallback() { + return 0; + } + } + + class Derived { + int run(int a) { + if (a > 1) + return a; + else + return 1; + } + }""" + ); + } + + @Test + void formats_whole_file_with_three_methods_and_mixed_control_flow() { + assertFormatsWholeFile( + """ + class Program{void boot(){start();}int choose(int a,int b){if(a>b)return a;else return b;}void spin(){for(i=0;i<3;i++)tick();}} + """, + """ + class Program { + void boot() { + start(); + } + + int choose(int a, int b) { + if (a > b) + return a; + else + return b; + } + + void spin() { + for (i = 0; i < 3; i++) + tick(); + } + }""" + ); + } + + @Test + void formats_whole_file_with_multiple_classes_and_else_if_in_second_class() { + assertFormatsWholeFile( + """ + class First{int id(){return 1;}} + class Second{int pick(int a,int b,int c){if(a>b)return a;else if(b>c)return b;else return c;}} + """, + """ + class First { + int id() { + return 1; + } + } + + class Second { + int pick(int a, int b, int c) { + if (a > b) + return a; + else if (b > c) + return b; + else + return c; + } + }""" + ); + } + + @Test + void formats_whole_file_with_package_imports_empty_class_and_worker_class() { + assertFormatsWholeFile( + """ + package app.core; + import java.util.List; + import java.util.Set; + class Empty{} + class Worker{void go(){for(i=0;i<1;i++){step();}}} + """, + """ + package app.core; + + import java.util.List; + import java.util.Set; + + class Empty {} + + class Worker { + void go() { + for (i = 0; i < 1; i++) { + step(); + } + } + }""" + ); + } + + @Test + void formats_whole_file_with_deeply_nested_if_for_and_blocks() { + assertFormatsWholeFile( + """ + class Deep{int run(int a,int b){if(a>b){for(i=0;i b) { + for (i = 0; i < a; i++) { + if (i == b) + return i; + tick(); + } + return a; + } + else if (a == b) { + for (i = 0;; i++) + step(); + } + else { + b--; + return b; + } + } + }""" + ); + } + + private static void assertFormatsWholeFile(String code, String expected) { + String formatted = formatASTFromRootToLeafs(code); + assertThat(formatted).isEqualTo(expected); + } + + private static String formatASTFromRootToLeafs(String code) { + CompilationUnit compilationUnit = StaticJavaParser.parse(code); + return engine.format(compilationUnit, "CompilationUnit"); + } + + private static List parseRules() { + ebnfLexer lexer = new ebnfLexer(CharStreams.fromString(ALL_RULES)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ebnfParser parser = new ebnfParser(tokens); + ebnfParser.RulelistContext ctx = parser.rulelist(); + RuleAstBuilder builder = new RuleAstBuilder(); + return builder.buildRules(ctx); + } +} From a008f2ffe5c9fcbb3e065c2c68d0b33c5b99ec96 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 22:40:51 +0300 Subject: [PATCH 17/29] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E=20javaparser,=20?= =?UTF-8?q?=D0=B4=D0=BE=20=D1=82=D0=BE=D0=B3=D0=BE,=20=D0=BA=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D1=8B=D0=B9=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=B8=D0=B2=D0=B0=D0=B5=D1=82=2025=20java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 64c66b0..e89b7c1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { antlr("org.antlr:antlr4:4.13.2") implementation("org.antlr:antlr4-runtime:4.13.2") - implementation("com.github.javaparser:javaparser-core:3.27.1") + implementation("com.github.javaparser:javaparser-core:3.28.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation(platform("org.junit:junit-bom:5.10.0")) From e7f78751b0ebf75003e4efa69cfbb9832ddf46b7 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 22:48:51 +0300 Subject: [PATCH 18/29] =?UTF-8?q?=D0=B5=D1=81=D0=BB=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=20=D0=B5=D1=81=D1=82=D1=8C,=20=D0=BD=D0=BE,=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80,=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=BA=D1=80=D0=B5=D1=82=D0=BD=D1=8B=D0=B9=20whileStmt=20?= =?UTF-8?q?=D0=BD=D0=B8=20=D0=BF=D0=BE=D0=B4=20=D0=BE=D0=B4=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=B4=D1=85=D0=BE=D0=B4=D0=B8=D1=82?= =?UTF-8?q?,=20=D1=82=D0=BE=20=D0=BC=D0=B0=D1=82=D1=87=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=20=D1=81=D1=80=D0=B0=D0=B7=D1=83=20=D0=BF=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=B5=D1=82.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83:=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=BD=D0=B5=D1=83=D0=B4=D0=B0=D1=87=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20DSL=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=B8=D1=82=D1=8C,=20=D1=87?= =?UTF-8?q?=D1=82=D0=BE=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B2=D1=81=D1=91=20=D1=80=D0=B0=D0=B2=D0=BD=D0=BE=20=D1=8F?= =?UTF-8?q?=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20JavaParser=20?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=D0=BE=D0=BC=20Statement,=20=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=B1=D0=B8=D0=BD=D0=B4=D0=B8=D1=82=D1=8C=20=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=20raw=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=B4=D1=83=D1=8E=D1=89=D0=B5=D0=B3=D0=BE=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ebnfFormatter/match/PatternMatcher.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java index bb4c0b4..14975a9 100644 --- a/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java +++ b/src/main/java/org/example/ebnfFormatter/match/PatternMatcher.java @@ -76,7 +76,7 @@ private boolean matchRuleRef(RuleRef ref, Object value, Bindings bindings) { return bindings.bind(ref.name(), new AppliedRuleValue(appliedRule)); } } - return false; + return bindRawValueIfMatchesDslType(ref.name(), value, bindings); } if (value == null) { @@ -95,6 +95,23 @@ private boolean matchRuleRef(RuleRef ref, Object value, Bindings bindings) { return bindRawValue(ref.name(), value, bindings); } + private boolean bindRawValueIfMatchesDslType(String refName, Object value, Bindings bindings) { + if (value == null) { + return false; + } + + try { + TypeSpec spec = typeRegistry.requireByDslName(refName); + if (!spec.javaType().isInstance(value)) { + return false; + } + } catch (IllegalArgumentException e) { + return false; + } + + return bindRawValue(refName, value, bindings); + } + private boolean matchNodePat(NodePat pat, Object value, Bindings bindings) { if (!(value instanceof Node node)) { return false; From 552030851ebccdda5b7cee56f1bcf07676767dc3 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 22:56:35 +0300 Subject: [PATCH 19/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=88=D0=B8=D0=BD=20=D0=B1=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20PrettyPriner=20Java=20Parser=20+=20=D1=81=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B0=D0=BB=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B1=D1=83=D1=8E?= =?UTF-8?q?=20=D0=BE=D1=82=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D0=BE=20Java=20Parser=20=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D1=83=20=D0=BD=D0=BE=D0=B4=D1=8B=20(=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B7=20tryRender)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../render/TemplateRenderer.java | 208 ++---------------- 1 file changed, 15 insertions(+), 193 deletions(-) diff --git a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java index aaaf2fb..79b9e5a 100644 --- a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java +++ b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java @@ -1,9 +1,16 @@ package org.example.ebnfFormatter.render; +import com.github.javaparser.ast.Modifier; import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.BinaryExpr; import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.UnaryExpr; import com.github.javaparser.ast.stmt.*; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.metamodel.PropertyMetaModel; import org.example.ebnfFormatter.match.AppliedRuleValue; import org.example.ebnfFormatter.match.Bindings; import org.example.ebnfFormatter.match.BoundValue; @@ -137,7 +144,7 @@ private void renderRawValue( } if (value instanceof Node node) { - renderNode(node, context); + renderNode(node, nestedRuleRenderer, context); return; } @@ -162,204 +169,19 @@ private void renderRawValue( context.appendText(String.valueOf(value)); } - private void renderNode(Node node, RenderContext context) { - if (node instanceof BlockStmt blockStmt) { - renderBlockStmt(blockStmt, context); + private void renderNode(Node node, NestedRuleRenderer nestedRuleRenderer, RenderContext context) { + Optional renderedByJavaParserType = nestedRuleRenderer.tryRender( + node.getClass().getSimpleName(), + node + ); + if (renderedByJavaParserType.isPresent()) { + context.appendText(renderedByJavaParserType.get()); return; } - if (node instanceof IfStmt ifStmt) { - renderIfStmt(ifStmt, context); - return; - } - - if (node instanceof ForStmt forStmt) { - renderForStmt(forStmt, context); - return; - } - - if (node instanceof MethodDeclaration methodDeclaration) { - renderMethodDeclaration(methodDeclaration, context); - return; - } - - if (node instanceof ReturnStmt returnStmt) { - renderReturnStmt(returnStmt, context); - return; - } - - if (node instanceof ExpressionStmt expressionStmt) { - renderExpressionStmt(expressionStmt, context); - return; - } - - context.appendText(node.toString()); } - private void renderReturnStmt(com.github.javaparser.ast.stmt.ReturnStmt stmt, RenderContext context) { - context.appendText("return"); - context.space(); - context.appendText(stmt.getExpression().map(Node::toString).orElse("")); - context.appendText(";"); - } - - private void renderExpressionStmt(com.github.javaparser.ast.stmt.ExpressionStmt stmt, RenderContext context) { - context.appendText(stmt.getExpression().toString()); - context.appendText(";"); - } - - private void renderMethodDeclaration(MethodDeclaration method, RenderContext context) { - if (!method.getModifiers().isEmpty()) { - for (int i = 0; i < method.getModifiers().size(); i++) { - if (i > 0) { - context.space(); - } - context.appendText(method.getModifiers().get(i).getKeyword().asString()); - } - context.space(); - } - - context.appendText(method.getType().toString()); - context.space(); - context.appendText(method.getNameAsString()); - context.appendText("("); - - for (int i = 0; i < method.getParameters().size(); i++) { - if (i > 0) { - context.appendText(", "); - } - context.appendText(method.getParameter(i).toString()); - } - - context.appendText(")"); - - if (method.getBody().isPresent()) { - context.space(); - renderBlockStmt(method.getBody().get(), context); - } else { - context.appendText(";"); - } - } - - private void renderBlockStmt(BlockStmt block, RenderContext context) { - context.appendText("{"); - context.newline(); - context.indent(); - - List statements = block.getStatements(); - for (int i = 0; i < statements.size(); i++) { - if (i > 0) { - context.newline(); - } - renderNode(statements.get(i), context); - } - - context.newline(); - context.dedent(); - context.appendText("}"); - } - - private void renderIfStmt(IfStmt stmt, RenderContext context) { - renderIfHeaderAndBody(stmt, context); - } - - private void renderIfHeaderAndBody(IfStmt stmt, RenderContext context) { - context.appendText("if"); - context.space(); - context.appendText("("); - context.appendText(stmt.getCondition().toString()); - context.appendText(")"); - - renderControlBody(stmt.getThenStmt(), context); - - stmt.getElseStmt().ifPresent(elseStmt -> { - if (elseStmt instanceof IfStmt nestedIf) { - context.newline(); - context.appendText("else"); - context.space(); - renderIfHeaderAndBody(nestedIf, context); - } else { - context.newline(); - context.appendText("else"); - renderControlBody(elseStmt, context); - } - }); - } - - private void renderForStmt(ForStmt stmt, RenderContext context) { - context.appendText("for"); - context.space(); - context.appendText("("); - - renderExpressionList(stmt.getInitialization(), context); - - context.appendText(";"); - - stmt.getCompare().ifPresent(compare -> { - context.space(); - context.appendText(compare.toString()); - }); - - context.appendText(";"); - - if (!stmt.getUpdate().isEmpty()) { - context.space(); - renderExpressionList(stmt.getUpdate(), context); - } - - context.appendText(")"); - - renderControlBody(stmt.getBody(), context); - } - - private void renderExpressionList(List expressions, RenderContext context) { - for (int i = 0; i < expressions.size(); i++) { - if (i > 0) { - context.appendText(", "); - } - context.appendText(expressions.get(i).toString()); - } - } - - private void renderControlBody(Statement body, RenderContext context) { - if (body instanceof BlockStmt blockStmt) { - context.space(); - renderBlockStmt(blockStmt, context); - return; - } - - context.newline(); - context.indent(); - renderNode(body, context); - context.dedent(); - } - - private boolean isPresent(Object value) { - if (value == null) { - return false; - } - - if (value instanceof Optional optional) { - return optional.isPresent(); - } - - if (value instanceof CharSequence chars) { - return !chars.isEmpty(); - } - - if (value instanceof Iterable iterable) { - return iterable.iterator().hasNext(); - } - - Class type = value.getClass(); - if (type.isArray()) { - return Array.getLength(value) > 0; - } - - return true; - } - private String stripQuantifierSuffix(String name) { if (name == null || name.isEmpty()) { return name; From d5abed8a0c78f4e99badc5f1eec0301b27bf48ea Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 22:58:51 +0300 Subject: [PATCH 20/29] =?UTF-8?q?=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=20=D0=B2=20while,=20=D0=BA=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BE=D0=B3=D0=BE=20=D0=BD=D0=B5=D1=82=20=D0=B2=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=D1=85=20=D1=83=D1=81?= =?UTF-8?q?=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=20=D0=BF=D1=80=D0=BE=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82,=20=D0=BA=D0=BE=D0=B4=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BF=D0=B0=D0=B4=D0=B0=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/ALotOfEndToEndTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java index 1ef2f52..4d2ae9b 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/ALotOfEndToEndTest.java @@ -721,6 +721,52 @@ else if (a == b) { ); } + @Test + void fff() { + assertFormatsWholeFile( + """ + class Deep { + int run(int a, int b) { + if (a > b) { + while (i < a) { + if (i == b) + return i;tick();++i; + } + return a; + } + else if (a == b) { + for (i = 0;; i++) + step(); + } + else { b--;return b;} + } + } + """, + """ + class Deep { + int run(int a, int b) { + if (a > b) { + while (i < a) { + if (i == b) + return i; + tick(); + ++i; + } + return a; + } + else if (a == b) { + for (i = 0;; i++) + step(); + } + else { + b--; + return b; + } + } + }""" + ); + } + private static void assertFormatsWholeFile(String code, String expected) { String formatted = formatASTFromRootToLeafs(code); assertThat(formatted).isEqualTo(expected); From 95822a994e70c23527fb4266e4e2ad8d0dfcfc58 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 22:59:50 +0300 Subject: [PATCH 21/29] =?UTF-8?q?=D1=80=D0=B0=D0=BD=D1=8C=D1=89=D0=B5=20?= =?UTF-8?q?=D0=B1=D1=8B=D0=BB=20=D1=85=D0=B0=D1=80=D0=B4=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?,=20=D0=B2=20=D0=BD=D1=91=D0=BC=20=D0=B1=D1=8B=D0=BB=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D0=B5=D0=BB=20=D0=BF=D1=80=D0=B8=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ebnfFormatter/runtime/ExamplesFromDocumentationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java index 9615c1f..c86a695 100644 --- a/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java +++ b/src/test/java/org/example/ebnfFormatter/runtime/ExamplesFromDocumentationTest.java @@ -155,7 +155,6 @@ public int sum(Parameter a,Parameter b,Parameter c,Parameter d) {} String expected = """ public int sum(Parameter a, Parameter b, Parameter c, Parameter d) { - }"""; String formatted = formatFirstNode(methodDeclarationRules, code, MethodDeclaration.class, "MethodDeclaration"); From 6c145e2829cd561781efed278adbcdc1b79a1154 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 23:01:51 +0300 Subject: [PATCH 22/29] =?UTF-8?q?50=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8=D0=BD=D0=B3=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B9,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D1=85=20=D0=BD?= =?UTF-8?q?=D0=B5=D1=82=20=D0=B2=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnknownNodesFallbackEndToEndTest.java | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesFallbackEndToEndTest.java diff --git a/src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesFallbackEndToEndTest.java b/src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesFallbackEndToEndTest.java new file mode 100644 index 0000000..4fb1733 --- /dev/null +++ b/src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesFallbackEndToEndTest.java @@ -0,0 +1,255 @@ +package org.example.ebnfFormatter.runtime; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.stmt.Statement; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.example.ebnfFormatter.dsl.RuleAstBuilder; +import org.example.ebnfFormatter.match.PatternMatcher; +import org.example.ebnfFormatter.model.RuleDef; +import org.example.ebnfFormatter.render.TemplateRenderer; +import org.example.ebnfLexer; +import org.example.ebnfParser; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +public class UnknownNodesFallbackEndToEndTest { + + private static final String ALL_RULES = """ + ::= CompilationUnit(packageDeclaration?=, imports=[*], types=[*]) + => ifpresent(PackageDeclaration, nl nl) + ifpresent(ImportDeclaration, join(, nl) nl nl) + join(, nl nl); + + ::= PackageDeclaration(name=) + => "package" sp ";"; + + ::= ImportDeclaration(name=) + => "import" sp ";"; + + ::= ClassOrInterfaceDeclaration(modifiers=[*], name=, members=[*]) + => ifpresent(Modifier, join(, "")) "class" sp sp "{" + ifpresent(MethodDeclaration, nl indent join(, nl nl) nl dedent) + "}"; + + ::= MethodDeclaration(modifiers=[*], type=, name=, parameters=[*], body=) + => ifpresent(Modifier, join(, "")) sp "(" + ifpresent(Parameter, join(, ", ")) + ")" sp ; + + ::= MethodDeclaration(modifiers=[*], type=, name=, parameters=[*]) + => ifpresent(Modifier, join(, "")) sp "(" + ifpresent(Parameter, join(, ", ")) + ")" ";"; + + ::= BlockStmt(statements=[*]) + => "{" nl indent join(, nl) nl dedent "}"; + + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => nl indent "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" dedent; + + ::= + => sp ; + + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => nl indent "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" ; + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ReturnStmt(expression=) + => "return" sp ";"; + + ::= ExpressionStmt(expression=) + => ";"; + """; + + private static final List ruleDefs = parseRules(); + private static final RuleRegistry registry = registryWithRules(); + private static final TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); + private static final PatternMatcher matcher = new PatternMatcher(typeRegistry, registry); + private static final FormatterEngine engine = new FormatterEngine(registry, matcher, new TemplateRenderer()); + private static final JavaParser javaParser = new JavaParser( + new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25) + ); + + @TestFactory + Stream formats_unknown_nodes_without_gluing_words() { + List cases = fallbackCases(); + + return cases.stream() + .map(testCase -> dynamicTest( + testCase.name(), + () -> assertFormatsSingleStatement(testCase.statement()) + )); + } + + private static List fallbackCases() { + return List.of( + new FallbackCase("while block", "while (i < limit) { i++; }"), + new FallbackCase("while expression body", "while (reader.ready()) line = reader.readLine();"), + new FallbackCase("do while block", "do { i++; } while (i < limit);"), + new FallbackCase("do while expression body", "do work(); while (running);"), + new FallbackCase("switch colon labels", "switch (kind) { case 1: work(); break; default: reset(); }"), + new FallbackCase("switch arrow labels", "switch (kind) { case 1 -> work(); default -> reset(); }"), + new FallbackCase("try catch", "try { work(); } catch (RuntimeException e) { handle(e); }"), + new FallbackCase("try finally", "try { work(); } finally { cleanup(); }"), + new FallbackCase("try multi catch finally", "try { work(); } catch (IllegalArgumentException | IllegalStateException e) { handle(e); } finally { cleanup(); }"), + new FallbackCase("try with resources", "try (Input input = open()) { read(input); }"), + new FallbackCase("synchronized block", "synchronized (lock) { work(); }"), + new FallbackCase("assert expression", "assert ready;"), + new FallbackCase("assert expression with message", "assert ready : \"not ready\";"), + new FallbackCase("throw statement", "throw new RuntimeException(\"boom\");"), + new FallbackCase("break statement", "break;"), + new FallbackCase("continue statement", "continue;"), + new FallbackCase("labeled while", "outer: while (running) { break outer; }"), + new FallbackCase("local int declaration", "int count = 0;"), + new FallbackCase("final local declaration", "final String name = user.name();"), + new FallbackCase("var local declaration", "var result = compute();"), + new FallbackCase("array creation declaration", "int[] values = new int[] {1, 2, 3};"), + new FallbackCase("nested array initializer", "int[][] matrix = {{1, 2}, {3, 4}};"), + new FallbackCase("generic collection declaration", "java.util.List names = new java.util.ArrayList<>();"), + new FallbackCase("lambda declaration", "Runnable task = () -> work();"), + new FallbackCase("method reference declaration", "java.util.function.Function trim = String::trim;"), + new FallbackCase("anonymous class declaration", "Object object = new Object() { public String toString() { return \"x\"; } };"), + new FallbackCase("instanceof pattern declaration", "boolean matches = value instanceof String text;"), + new FallbackCase("ternary declaration", "int selected = flag ? left : right;"), + new FallbackCase("cast declaration", "String casted = (String) value;"), + new FallbackCase("shift declaration", "long mask = 1L << shift;"), + new FallbackCase("assignment expression", "a = b + c * d;"), + new FallbackCase("array assignment expression", "arr[index++] += delta;"), + new FallbackCase("prefix increment expression", "++counter;"), + new FallbackCase("postfix decrement expression", "counter--;"), + new FallbackCase("field assignment expression", "this.field = value;"), + new FallbackCase("super method call expression", "super.toString();"), + new FallbackCase("chained call expression", "builder.append(\"a\").append(\"b\");"), + new FallbackCase("stream method references", "names.stream().map(String::trim).forEach(this::use);"), + new FallbackCase("lambda in constructor call", "new Thread(() -> run()).start();"), + new FallbackCase("optional method reference", "java.util.Optional.ofNullable(name).ifPresent(System.out::println);"), + new FallbackCase("local class declaration", "class Local { void go() {} }"), + new FallbackCase("if with unknown direct bodies", "if (ready) while (running) { tick(); } else do { sleep(); } while (waiting);"), + new FallbackCase("for with unknown switch body", "for (i = 0; i < limit; i++) switch (i) { case 0 -> start(); default -> tick(); }"), + new FallbackCase("for each statement", "for (String item : items) { use(item); }"), + new FallbackCase("try catch rethrow", "try { risky(); } catch (Exception e) { throw new IllegalStateException(e); }"), + new FallbackCase("switch multiple arrow labels", "switch (mode) { case A, B -> run(); case C -> stop(); default -> reset(); }"), + new FallbackCase("while assignment condition", "while ((line = reader.readLine()) != null) { process(line); }"), + new FallbackCase("do while assignment body", "do { current = current.next(); } while (current != null);"), + new FallbackCase("synchronized computed lock", "synchronized (cache.compute(key, (k, v) -> v)) { touch(); }"), + new FallbackCase("assert lambda expression", "assert items.stream().allMatch(x -> x != null) : items;") + ); + } + + private static void assertFormatsSingleStatement(String statement) { + String code = """ + class Sample { void run() { %s } } + """.formatted(statement); + + String expected = """ + class Sample { + void run() { + %s + } + }""".formatted(indent(parseStatement(statement).toString(), 8)); + + assertThat(formatASTFromRootToLeafs(code)).isEqualTo(expected); + } + + private static String indent(String text, int spaces) { + String indentation = " ".repeat(spaces); + return text.lines() + .map(line -> indentation + line) + .collect(Collectors.joining("\n")); + } + + private static String formatASTFromRootToLeafs(String code) { + CompilationUnit compilationUnit = parseCompilationUnit(code); + return engine.format(compilationUnit, "CompilationUnit"); + } + + private static CompilationUnit parseCompilationUnit(String code) { + ParseResult result = javaParser.parse(code); + return result.getResult() + .orElseThrow(() -> new IllegalArgumentException(result.getProblems().toString())); + } + + private static Statement parseStatement(String statement) { + ParseResult result = javaParser.parseStatement(statement); + return result.getResult() + .orElseThrow(() -> new IllegalArgumentException(result.getProblems().toString())); + } + + private static RuleRegistry registryWithRules() { + RuleRegistry ruleRegistry = new RuleRegistry(); + ruleRegistry.registerAll(ruleDefs); + return ruleRegistry; + } + + private static List parseRules() { + ebnfLexer lexer = new ebnfLexer(CharStreams.fromString(ALL_RULES)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ebnfParser parser = new ebnfParser(tokens); + ebnfParser.RulelistContext ctx = parser.rulelist(); + RuleAstBuilder builder = new RuleAstBuilder(); + return builder.buildRules(ctx); + } + + private record FallbackCase(String name, String statement) { + } +} From 19300c61cd7b08e633de6fd9ea1505b030ec0483 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 1 May 2026 23:07:35 +0300 Subject: [PATCH 23/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83=20=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=B2=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9=20(?= =?UTF-8?q?=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=BD=D1=81=D1=82?= =?UTF-8?q?=D1=80=D1=83=D0=BA=D1=86=D0=B8=D0=B9,=20=D0=BD=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=B5=20=D0=BD=D0=B5=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D1=8B=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnknownNodesWholeFileEndToEndTest.java | 437 ++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesWholeFileEndToEndTest.java diff --git a/src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesWholeFileEndToEndTest.java b/src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesWholeFileEndToEndTest.java new file mode 100644 index 0000000..7a34562 --- /dev/null +++ b/src/test/java/org/example/ebnfFormatter/runtime/UnknownNodesWholeFileEndToEndTest.java @@ -0,0 +1,437 @@ +package org.example.ebnfFormatter.runtime; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.example.ebnfFormatter.dsl.RuleAstBuilder; +import org.example.ebnfFormatter.match.PatternMatcher; +import org.example.ebnfFormatter.model.RuleDef; +import org.example.ebnfFormatter.render.TemplateRenderer; +import org.example.ebnfLexer; +import org.example.ebnfParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UnknownNodesWholeFileEndToEndTest { + + private static final String ALL_RULES = """ + ::= CompilationUnit(packageDeclaration?=, imports=[*], types=[*]) + => ifpresent(PackageDeclaration, nl nl) + ifpresent(ImportDeclaration, join(, nl) nl nl) + join(, nl nl); + + ::= PackageDeclaration(name=) + => "package" sp ";"; + + ::= ImportDeclaration(name=) + => "import" sp ";"; + + ::= ClassOrInterfaceDeclaration(modifiers=[*], name=, members=[*]) + => ifpresent(Modifier, join(, "")) "class" sp sp "{" + ifpresent(MethodDeclaration, nl indent join(, nl nl) nl dedent) + "}"; + + ::= MethodDeclaration(modifiers=[*], type=, name=, parameters=[*], body=) + => ifpresent(Modifier, join(, "")) sp "(" + ifpresent(Parameter, join(, ", ")) + ")" sp ; + + ::= MethodDeclaration(modifiers=[*], type=, name=, parameters=[*]) + => ifpresent(Modifier, join(, "")) sp "(" + ifpresent(Parameter, join(, ", ")) + ")" ";"; + + ::= BlockStmt(statements=[*]) + => "{" nl indent join(, nl) nl dedent "}"; + + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => nl indent "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" dedent; + + ::= + => sp ; + + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => nl indent "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" dedent; + + ::= ForStmt(initialization=[*], compare?=, update=[*], body=) + => "for" sp "(" + ifpresent(InitExpr, join(, ", ")) + ";" ifpresent(CompareExpr, sp ) + ";" ifpresent(UpdateExpr, sp join(, ", ")) + ")" ; + + ::= BlockStmt(statements=[*]) + => sp "{" nl indent join(, nl) nl dedent "}"; + + ::= ExpressionStmt(expression=) + => nl indent ";" dedent; + + ::= ReturnStmt(expression=) + => nl indent "return" sp ";" dedent; + + ::= ReturnStmt(expression=) + => "return" sp ";"; + + ::= ExpressionStmt(expression=) + => ";"; + """; + + private static final String BINARY_EXPR_RULES = """ + ::= ReturnStmt(expression=) + => "return" sp ";"; + + ::= BinaryExpr(left=, right=) + => sp "[binary]" sp ; + """; + + private static final List ruleDefs = parseRules(ALL_RULES); + private static final RuleRegistry registry = registryWithRules(ruleDefs); + private static final TypeRegistryUniversal typeRegistry = new TypeRegistryUniversal(); + private static final PatternMatcher matcher = new PatternMatcher(typeRegistry, registry); + private static final FormatterEngine engine = new FormatterEngine(registry, matcher, new TemplateRenderer()); + + private static final List binaryExprRuleDefs = parseRules(BINARY_EXPR_RULES); + private static final RuleRegistry binaryExprRegistry = registryWithRules(binaryExprRuleDefs); + private static final TypeRegistryUniversal binaryExprTypeRegistry = new TypeRegistryUniversal(); + private static final PatternMatcher binaryExprMatcher = new PatternMatcher(binaryExprTypeRegistry, binaryExprRegistry); + private static final FormatterEngine binaryExprEngine = new FormatterEngine( + binaryExprRegistry, + binaryExprMatcher, + new TemplateRenderer() + ); + + private static final JavaParser javaParser = new JavaParser( + new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25) + ); + + @Test + void formats_block_with_while_and_return_without_expression() { + assertFormatsWholeFile( + """ + class Flow{void run(){while(irun();case C->stop();default->reset();}done();}} + """, + """ + class Switcher { + void run() { + switch(mode) { + case A, B -> + run(); + case C -> + stop(); + default -> + reset(); + } + done(); + } + }""" + ); + } + + @Test + void formats_block_with_try_catch_finally() { + assertFormatsWholeFile( + """ + class Worker{void run(){try{work();}catch(IllegalArgumentException|IllegalStateException e){handle(e);}finally{cleanup();}done();}} + """, + """ + class Worker { + void run() { + try { + work(); + } catch (IllegalArgumentException | IllegalStateException e) { + handle(e); + } finally { + cleanup(); + } + done(); + } + }""" + ); + } + + @Test + void formats_block_with_try_with_resources() { + assertFormatsWholeFile( + """ + class Reader{void run(){try(Input input=open()){read(input);}closeCount++;}} + """, + """ + class Reader { + void run() { + try (Input input = open()) { + read(input); + } + closeCount++; + } + }""" + ); + } + + @Test + void formats_block_with_synchronized_and_assert() { + assertFormatsWholeFile( + """ + class Guarded{void run(){synchronized(lock){work();}assert ready:"not ready";done();}} + """, + """ + class Guarded { + void run() { + synchronized (lock) { + work(); + } + assert ready : "not ready"; + done(); + } + }""" + ); + } + + @Test + void formats_block_with_for_each_statement() { + assertFormatsWholeFile( + """ + class Iteration{void run(){for(String item:items){use(item);}done();}} + """, + """ + class Iteration { + void run() { + for (String item : items) { + use(item); + } + done(); + } + }""" + ); + } + + @Test + void formats_block_with_lambda_and_method_reference_declarations() { + assertFormatsWholeFile( + """ + class LocalStuff{void run(){Runnable task=()->work();java.util.function.Function trim=String::trim;task.run();}} + """, + """ + class LocalStuff { + void run() { + Runnable task = () -> work(); + java.util.function.Function trim = String::trim; + task.run(); + } + }""" + ); + } + + @Test + void formats_block_with_instanceof_pattern_and_throw() { + assertFormatsWholeFile( + """ + class PatternUse{void run(Object value){if(value instanceof String text)use(text);throw new RuntimeException("boom");}} + """, + """ + class PatternUse { + void run(Object value) { + if (value instanceof String text) + use(text); + throw new RuntimeException("boom"); + } + }""" + ); + } + + @Test + void formats_unknown_if_as_raw_when_then_and_else_are_unknown() { + assertFormatsWholeFile( + """ + class Branch{void run(){if(ready)while(running){tick();}else do{sleep();}while(waiting);done();}} + """, + """ + class Branch { + void run() { + if (ready) + while (running) { + tick(); + } + else + do { + sleep(); + } while (waiting); + done(); + } + }""" + ); + } + + @Test + void formats_known_for_with_unknown_switch_body() { + assertFormatsWholeFile( + """ + class Mixed{void run(){for(i=0;istart();default->tick();}done();}} + """, + """ + class Mixed { + void run() { + for (i = 0; i < limit; i++) switch(i) { + case 0 -> + start(); + default -> + tick(); + } + done(); + } + }""" + ); + } + + @Test + void formats_package_imports_and_unknown_constructs_in_multiple_methods() { + assertFormatsWholeFile( + """ + package demo; + import java.util.List; + class Mixed{void loop(){while((line=reader.readLine())!=null){process(line);}}void choose(){switch(mode){case A,B->run();default->reset();}}} + """, + """ + package demo; + + import java.util.List; + + class Mixed { + void loop() { + while ((line = reader.readLine()) != null) { + process(line); + } + } + + void choose() { + switch(mode) { + case A, B -> + run(); + default -> + reset(); + } + } + }""" + ); + } + + @Test + void raw_expression_can_be_rendered_by_concrete_binary_expr_rule() { + Node node = parseStatement("return a + b;"); + + assertThat(binaryExprEngine.format(node, "ReturnStmt")) + .isEqualTo("return a [binary] b;"); + } + + private static void assertFormatsWholeFile(String code, String expected) { + assertThat(formatASTFromRootToLeafs(code)).isEqualTo(expected); + } + + private static String formatASTFromRootToLeafs(String code) { + return engine.format(parseCompilationUnit(code), "CompilationUnit"); + } + + private static RuleRegistry registryWithRules(List rules) { + RuleRegistry ruleRegistry = new RuleRegistry(); + ruleRegistry.registerAll(rules); + return ruleRegistry; + } + + private static CompilationUnit parseCompilationUnit(String code) { + ParseResult result = javaParser.parse(code); + return result.getResult() + .orElseThrow(() -> new IllegalArgumentException(result.getProblems().toString())); + } + + private static Node parseStatement(String statement) { + ParseResult result = javaParser.parseStatement(statement); + return result.getResult() + .orElseThrow(() -> new IllegalArgumentException(result.getProblems().toString())); + } + + private static List parseRules(String rules) { + ebnfLexer lexer = new ebnfLexer(CharStreams.fromString(rules)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ebnfParser parser = new ebnfParser(tokens); + ebnfParser.RulelistContext ctx = parser.rulelist(); + RuleAstBuilder builder = new RuleAstBuilder(); + return builder.buildRules(ctx); + } +} From 812fc36a7fef9bb94702d4febb142c3a5cfdb156 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 4 May 2026 16:57:20 +0300 Subject: [PATCH 24/29] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D1=81=D0=B8=D0=BD=D1=82=D0=B0=D0=BA=D1=81=D0=B8=D1=81=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20sql=20=D0=BF=D0=BE=D0=B4=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B8=D0=BF=D0=B0=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=83=D0=B6=D0=B5=20=D0=BD=D0=B5=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=D0=B7=D1=83=D0=B5=D1=82=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/antlr/SqlLikeRequestLexer.g4 | 62 ---------------- src/main/antlr/SqlLikeRequestParser.g4 | 97 -------------------------- 2 files changed, 159 deletions(-) delete mode 100644 src/main/antlr/SqlLikeRequestLexer.g4 delete mode 100644 src/main/antlr/SqlLikeRequestParser.g4 diff --git a/src/main/antlr/SqlLikeRequestLexer.g4 b/src/main/antlr/SqlLikeRequestLexer.g4 deleted file mode 100644 index 878a56d..0000000 --- a/src/main/antlr/SqlLikeRequestLexer.g4 +++ /dev/null @@ -1,62 +0,0 @@ -lexer grammar SqlLikeRequestLexer; - - -SELECT : [sS][eE][lL][eE][cC][tT]; -WHERE : [wW][hH][eE][rR][eE]; -FORMAT : [fF][oO][rR][mM][aA][tT] -> pushMode(FORMAT_MODE); -IF : [iI][fF]; -ELSE : [eE][lL][sS][eE]; -AS : [aA][sS]; - -AND : [aA][nN][dD]; -OR : [oO][rR]; -NOT : [nN][oO][tT]; - -TRUE : [tT][rR][uU][eE]; -FALSE : [fF][aA][lL][sS][eE]; -NULL : [nN][uU][lL][lL]; - -EQ : '=='; -ASSIGN : '=' ; -NEQ : '!='; -LTE : '<='; -GTE : '>='; -LT : '<'; -GT : '>'; -SEMI : ';'; - -DOT : '.'; -LPAREN : '('; -RPAREN : ')'; -LBRACE : '{' ; -RBRACE : '}' ; - -ID - : [a-zA-Z_][a-zA-Z0-9_]* - ; - -REPLACEMENT - : '$'[a-zA-Z_][a-zA-Z0-9_]* - ; - -NUMBER - : [0-9]+ ('.' [0-9]+)? - ; - -// строка или строка с \... внутри -STRING - : '"' ( '\\' . | ~["\\] )* '"' - | '\'' ( '\\' . | ~['\\] )* '\'' - ; - -WS - : [ \t\r\n]+ -> skip - ; - -mode FORMAT_MODE; -PLACEHOLDER - : '$' [a-zA-Z_][a-zA-Z0-9_]* ('.' [a-zA-Z_][a-zA-Z0-9_]*)* - ; -TEXT - : ~[$]+ - ; \ No newline at end of file diff --git a/src/main/antlr/SqlLikeRequestParser.g4 b/src/main/antlr/SqlLikeRequestParser.g4 deleted file mode 100644 index 983084f..0000000 --- a/src/main/antlr/SqlLikeRequestParser.g4 +++ /dev/null @@ -1,97 +0,0 @@ -grammar SqlLikeRequestParser; - -options { tokenVocab=SqlLikeRequestLexer; } - -query - : formatStmt EOF - ; - -formatStmt - : selectStmt (FORMAT formatString)? - ; - - -selectStmt - : SELECT selectTarget (WHERE expr)? - ; - -formatString - : formatPart* - ; - -formatPart - : PLACEHOLDER - | TEXT - ; -blockAssign - : LBRACE assignStmt* RBRACE - ; - -assignStmt - : REPLACEMENT (DOT ID)* ASSIGN expr SEMI - | ID (DOT ID)* ASSIGN expr SEMI - ; - -selectTarget - : typeName (AS REPLACEMENT)? - ; - -typeName - : ID - ; - -expr - : orExpr - ; - -orExpr - : andExpr (OR andExpr)* - ; - -andExpr - : notExpr (AND notExpr)* - ; - -notExpr - : NOT notExpr - | comparisonExpr - ; - -comparisonExpr - : additiveExpr (compOp additiveExpr)? - ; - -additiveExpr - : primary - ; - -primary - : LPAREN expr RPAREN - | literal - | qualifiedName - ; - -compOp - : EQ - | NEQ - | LT - | LTE - | GT - | GTE - ; - -// Имя с точками, например IfStmt.thenStmt и т д -qualifiedName - : (ID | REPLACEMENT) (DOT ID)* - ; - -literal - : STRING - | NUMBER - | TRUE - | FALSE - | NULL - ; - -// правила Lexera - From cc162a6af9b5b02110fc80a73428d9e21cd7b100 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 May 2026 15:10:00 +0300 Subject: [PATCH 25/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D1=83=D1=8E=20=D0=B8=D0=BD=D1=81?= =?UTF-8?q?=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8=D1=8E=20=D0=BD=D0=B0=20?= =?UTF-8?q?=D1=82=D0=BE,=20=D0=BA=D0=B0=D0=BA=20=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0,=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B3=D0=B4=D0=B0=20=D1=82=D1=8B=20=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D0=B5=D1=88=D1=8C,=20=D0=BA=D0=B0=D0=BA=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=BE=D0=B4=D0=B0=20AST=20=D0=B2=20JavaParser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HowToWriteRules.md | 215 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 HowToWriteRules.md diff --git a/HowToWriteRules.md b/HowToWriteRules.md new file mode 100644 index 0000000..115b22f --- /dev/null +++ b/HowToWriteRules.md @@ -0,0 +1,215 @@ +# Как писать правила, если не знаешь имя JavaParser AST-ноды + +Эта инструкция помогает перейти от Java-кода к DSL-правилу форматтера, когда непонятно, как JavaParser называет нужную конструкцию и какие поля у нее есть. + +## Короткий алгоритм + +1. Берём минимальный Java-код с нужной конструкцией. +2. Распарсиваем его JavaParser-ом. +3. Печатаем дерево AST: имена классов нод и их `.toString()`. +4. Находим ноду, которая соответствует нужной конструкции. +5. Печатаем properties этой ноды. +6. Используем имя класса ноды в DSL pattern. +7. Используем имена properties как имена полей в DSL. + +## Минимальный код для поиска имени ноды + +Вставляем такой код: + +```java +JavaParser parser = new JavaParser( + new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25) +); + +String code = """ + class Sample { + void run() { + while (i < limit) { + i++; + } + } + } + """; + +CompilationUnit compilationUnit = parser.parse(code) + .getResult() + .orElseThrow(); + +compilationUnit.findAll(Node.class).forEach(node -> { + String source = node.toString().replace('\n', ' '); + System.out.println(node.getClass().getSimpleName() + " -> " + source); +}); +``` + +В выводе будет: + +```text +CompilationUnit -> class Sample { void run() { while (i < limit) { i++; } } } +ClassOrInterfaceDeclaration -> class Sample { void run() { while (i < limit) { i++; } } } +SimpleName -> Sample +MethodDeclaration -> void run() { while (i < limit) { i++; } } +SimpleName -> run +VoidType -> void +BlockStmt -> { while (i < limit) { i++; } } +WhileStmt -> while (i < limit) { i++; } +BinaryExpr -> i < limit +NameExpr -> i +SimpleName -> i +NameExpr -> limit +SimpleName -> limit +BlockStmt -> { i++; } +ExpressionStmt -> i++; +UnaryExpr -> i++ +NameExpr -> i +SimpleName -> i +``` + +Значит для `while` имя JavaParser-ноды: + +```text +WhileStmt +``` + +## Как посмотреть поля ноды + +Когда имя ноды найдено, нужно понять, какие поля можно использовать в DSL. Дописываем к предыдущему коду (пример для WhileStmt): + +```java +Node node = compilationUnit.findFirst(WhileStmt.class).orElseThrow(); + +for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels()) { + String name = property.getName(); + + if (List.of( + "metaModel", // описание модели самой ноды + "range", // позиция в исходном файле + "tokenRange", // диапазон токенов + "parsed", // получена ли нода парсером или создана програмно + "comment", // комментарий, прикрепленный к ноде + "orphanComments", // комментарий, который был рядом с нодой + "allContainedComments", // все комментарии в поддереве ноды + "childNodes", // все дети ноды без имен полей + "parentNode" // родительская нода + ).contains(name)) { + continue; + } + + Object value = property.getValue(node); + System.out.println(name + " -> " + value); +} +``` + +Вывод (поля для `WhileStmt`): + +```text +body -> { + i++; +} +condition -> i < limit +``` + + +## Как, зная имя ноды, написать DSL-правило + +Если JavaParser class называется: + +```text +WhileStmt +``` + +то в DSL pattern пишем: + +```ebnf +WhileStmt(...) +``` + +Если property называется: + +```text +condition +body +``` + +то в DSL пишем: + +```ebnf +condition=, body= +``` + +Полный пример: + +```ebnf + ::= WhileStmt(condition=, body=) + => "while" sp "(" ")" sp ; +``` + +## Списки и optional-поля + +Если property содержит список, в DSL используется: + +```ebnf +statements=[*] +``` + +Пример: + +```ebnf + ::= BlockStmt(statements=[*]) + => "{" nl indent join(, nl) nl dedent "}"; +``` + +Если property может быть, а может отсутствовать, используется `?` у имени поля: + +```ebnf +elseStmt?= +``` + +Пример: + +```ebnf + ::= IfStmt(condition=, thenStmt=, elseStmt?=) + => "if" sp "(" ")" + ifpresent(ElseStmt, nl "else" ); +``` + +## Важно: если два поля могут содержать разные значения, не называй placeholder одинаково. + +Плохо: + +```ebnf + ::= BinaryExpr(left=, right=) + => sp "+" sp ; +``` + +Такой rule может конфликтовать в bindings, потому что левое и правое выражения разные. + +Лучше: + +```ebnf + ::= BinaryExpr(left=, right=) + => sp "+" sp ; +``` + +# Итог +## Быстрый шаблон для нового правила + +1. Найти в AST: + +```text +SomeNode +``` + +2. Посмотреть properties: + +```text +left +operator +right +``` + +3. Написать правило: + +```ebnf + ::= SomeNode(left=, operator=, right=) + => sp sp ; +``` \ No newline at end of file From 370e583612106f5dc085b83cae810041d965e4de Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 May 2026 17:15:01 +0300 Subject: [PATCH 26/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B2=20placeholder=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BD=D0=B5=20=D1=8F=D0=B2=D0=BB=D1=8F=D1=8E?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=BD=D0=BE=D0=B4=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../render/TemplateRenderer.java | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java index 79b9e5a..8b4e905 100644 --- a/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java +++ b/src/main/java/org/example/ebnfFormatter/render/TemplateRenderer.java @@ -11,6 +11,7 @@ import com.github.javaparser.ast.stmt.*; import com.github.javaparser.ast.type.PrimitiveType; import com.github.javaparser.metamodel.PropertyMetaModel; +import com.github.javaparser.printer.Stringable; import org.example.ebnfFormatter.match.AppliedRuleValue; import org.example.ebnfFormatter.match.Bindings; import org.example.ebnfFormatter.match.BoundValue; @@ -166,7 +167,51 @@ private void renderRawValue( return; } - context.appendText(String.valueOf(value)); + context.appendText(valueToSource(value)); + } + + private String valueToSource(Object value) { +// if (value instanceof BinaryExpr.Operator operator) { +// return operator.asString(); +// } +// +// if (value instanceof AssignExpr.Operator operator) { +// return operator.asString(); +// } +// +// if (value instanceof UnaryExpr.Operator operator) { +// return operator.asString(); +// } +// +// if (value instanceof PrimitiveType.Primitive primitive) { +// return primitive.asString(); +// } + + if (value instanceof Stringable stringable) { + return stringable.asString(); + } + + + if (value instanceof Modifier.Keyword keyword) { + return keyword.asString(); + } +// DEFAULT("default"), +// PUBLIC("public"), +// PROTECTED("protected"), +// PRIVATE("private"), +// ABSTRACT("abstract"), +// STATIC("static"), +// FINAL("final"), +// TRANSIENT("transient"), +// VOLATILE("volatile"), +// SYNCHRONIZED("synchronized"), +// NATIVE("native"), +// STRICTFP("strictfp"), +// TRANSITIVE("transitive"), +// SEALED("sealed"), +// NON_SEALED("non-sealed"); + + return String.valueOf(value); } private void renderNode(Node node, NestedRuleRenderer nestedRuleRenderer, RenderContext context) { From 74432fc5e78022ce34213d417179d1afca7bbea1 Mon Sep 17 00:00:00 2001 From: Igor Date: Wed, 6 May 2026 17:28:43 +0300 Subject: [PATCH 27/29] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=BD=D0=B0=20=D1=82?= =?UTF-8?q?=D0=B5=20=D1=81=D0=B8=D1=82=D1=83=D0=B0=D1=86=D0=B8=D0=B8,=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=B3=D0=B4=D0=B0=20=D0=B2=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=D0=B0=D1=85=20=D1=83=D1=87=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D1=83=D1=8E=D1=82=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B,=20=D0=BD=D0=B5=20=D1=8F=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D1=8E=D1=88=D0=B8=D0=B5=D1=81=D1=8F=20=D0=BD=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/FormatCodeThatNotNodeTest.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 src/test/java/org/example/ebnfFormatter/runtime/FormatCodeThatNotNodeTest.java diff --git a/src/test/java/org/example/ebnfFormatter/runtime/FormatCodeThatNotNodeTest.java b/src/test/java/org/example/ebnfFormatter/runtime/FormatCodeThatNotNodeTest.java new file mode 100644 index 0000000..9565e94 --- /dev/null +++ b/src/test/java/org/example/ebnfFormatter/runtime/FormatCodeThatNotNodeTest.java @@ -0,0 +1,158 @@ +package org.example.ebnfFormatter.runtime; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.BinaryExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.type.PrimitiveType; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.example.ebnfFormatter.dsl.RuleAstBuilder; +import org.example.ebnfFormatter.match.PatternMatcher; +import org.example.ebnfFormatter.model.RuleDef; +import org.example.ebnfFormatter.render.TemplateRenderer; +import org.example.ebnfLexer; +import org.example.ebnfParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FormatCodeThatNotNodeTest { + private static final String NOT_NODE_RULES = """ + ::= ClassOrInterfaceDeclaration(modifiers=[*], name=) + => join(, " ") sp "class" sp ; + + ::= Modifier(keyword=) + => ; + + ::= PrimitiveType(type=) + => ; + + ::= BinaryExpr(left=, operator=, right=) + => sp sp ; + + ::= AssignExpr(target=, operator=, value=) + => sp sp ; + + ::= UnaryExpr(operator=, expression=) + => ; + """; + + private static final List ontNodeRuleDefs = parseRules(NOT_NODE_RULES); + private static final RuleRegistry ontNodeRegistry = registryWithRules(ontNodeRuleDefs); + private static final TypeRegistryUniversal ontNodeTypeRegistry = new TypeRegistryUniversal(); + private static final PatternMatcher ontNodeMatcher = new PatternMatcher( + ontNodeTypeRegistry, + ontNodeRegistry + ); + private static final FormatterEngine ontNodeEngine = new FormatterEngine( + ontNodeRegistry, + ontNodeMatcher, + new TemplateRenderer() + ); + + private static final JavaParser javaParser = new JavaParser( + new ParserConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_25) + ); + + + @Test + void formats_modifier_keyword_final_that_is_not_node() { + Modifier modifier = parseFirstNode("final class Sample {}", Modifier.class); + + assertThat(ontNodeEngine.format(modifier, "Modifier")).isEqualTo("final"); + } + + @Test + void formats_public_final_keywords_from_class_modifiers() { + ClassOrInterfaceDeclaration classDeclaration = parseFirstNode( + "public final class Sample {}", + ClassOrInterfaceDeclaration.class + ); + + assertThat(ontNodeEngine.format(classDeclaration, "ClassOrInterfaceDeclaration")) + .isEqualTo("public final class Sample"); + } + + @Test + void formats_primitive_type_value_that_is_not_node() { + PrimitiveType primitiveType = parseFirstNode( + "class Sample { int run() { return 1; } }", + PrimitiveType.class + ); + + assertThat(ontNodeEngine.format(primitiveType, "PrimitiveType")).isEqualTo("int"); + } + + @Test + void formats_binary_operator_value_that_is_not_node() { + BinaryExpr binaryExpr = parseFirstStatementNode("return a == b;", BinaryExpr.class); + + assertThat(ontNodeEngine.format(binaryExpr, "BinaryExpr")).isEqualTo("a == b"); + } + + @Test + void formats_assign_operator_value_that_is_not_node() { + AssignExpr assignExpr = parseFirstStatementNode("a += b;", AssignExpr.class); + + assertThat(ontNodeEngine.format(assignExpr, "AssignExpr")).isEqualTo("a += b"); + } + + @Test + void formats_unary_operator_value_that_is_not_node() { + UnaryExpr unaryExpr = parseFirstStatementNode("++counter;", UnaryExpr.class); + + assertThat(ontNodeEngine.format(unaryExpr, "UnaryExpr")).isEqualTo("++counter"); + } + + + private static RuleRegistry registryWithRules(List rules) { + RuleRegistry ruleRegistry = new RuleRegistry(); + ruleRegistry.registerAll(rules); + return ruleRegistry; + } + + private static CompilationUnit parseCompilationUnit(String code) { + ParseResult result = javaParser.parse(code); + return result.getResult() + .orElseThrow(() -> new IllegalArgumentException(result.getProblems().toString())); + } + + private static Node parseStatement(String statement) { + ParseResult result = javaParser.parseStatement(statement); + return result.getResult() + .orElseThrow(() -> new IllegalArgumentException(result.getProblems().toString())); + } + + private static T parseFirstNode(String code, Class nodeType) { + return parseCompilationUnit(code) + .findFirst(nodeType) + .orElseThrow(() -> new IllegalArgumentException("Cannot find " + nodeType.getSimpleName())); + } + + private static T parseFirstStatementNode(String statement, Class nodeType) { + return parseStatement(statement) + .findFirst(nodeType) + .orElseThrow(() -> new IllegalArgumentException("Cannot find " + nodeType.getSimpleName())); + } + + + + private static List parseRules(String rules) { + ebnfLexer lexer = new ebnfLexer(CharStreams.fromString(rules)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + ebnfParser parser = new ebnfParser(tokens); + ebnfParser.RulelistContext ctx = parser.rulelist(); + RuleAstBuilder builder = new RuleAstBuilder(); + return builder.buildRules(ctx); + } + +} From 5a04a5d740eb273cc7c15118c4dceb04f5a5b3f4 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 8 May 2026 22:00:10 +0300 Subject: [PATCH 28/29] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=20=D0=B3=D0=B0=D0=B9=D0=B4=20=D1=87=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=20=D1=82=D0=BE,=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BA=20=D0=BF=D0=B8=D1=81=D0=B0=D1=82=D1=8C=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D0=B0=20=D0=BD=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B5=20Node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HowToWriteRules.md | 207 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 196 insertions(+), 11 deletions(-) diff --git a/HowToWriteRules.md b/HowToWriteRules.md index 115b22f..8dbd78f 100644 --- a/HowToWriteRules.md +++ b/HowToWriteRules.md @@ -172,7 +172,7 @@ elseStmt?= ifpresent(ElseStmt, nl "else" ); ``` -## Важно: если два поля могут содержать разные значения, не называй placeholder одинаково. +## Важно: если два поля могут содержать разные значения, не нужно placeholder называть одинаково. Плохо: @@ -190,26 +190,211 @@ elseStmt?= => sp "+" sp ; ``` +## Если часть конструкции не Node + +Не все properties JavaParser являются нодами. Например: +```text +Modifier.keyword -> FINAL +PrimitiveType.type -> INT +BinaryExpr.operator -> PLUS +AssignExpr.operator -> PLUS +UnaryExpr.operator -> PREFIX_INCREMENT +``` + +Чтобы понять, что именно лежит в таком поле, нужно печатать не только значение, но и Java-класс этого значения (т. к. одно и то же текстовое значение может означать разные вещи в разных классах JavaParser). +Например: +```text +BinaryExpr.operator -> PLUS +AssignExpr.operator -> PLUS +``` +Одинаковое название PLUS в разных классах +```text +com.github.javaparser.ast.expr.BinaryExpr.Operator +com.github.javaparser.ast.expr.AssignExpr.Operator +``` +Имеют разный смысл +```text +a + b // BinaryExpr.Operator.PLUS +a += b // AssignExpr.Operator.PLUS +``` + +Для определения названий "не Node конструкций" подойдёт такой набор функций +```java +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.metamodel.PropertyMetaModel; + +import java.util.List; +import java.util.Optional; + +private static void printNonNodeProperties(Node node) { + for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels()) { + String name = property.getName(); + + if (List.of( + "metaModel", // описание модели самой ноды + "range", // позиция в исходном файле + "tokenRange", // диапазон токенов + "parsed", // получена ли нода парсером или создана програмно + "comment", // комментарий, прикрепленный к ноде + "orphanComments", // комментарий, который был рядом с нодой + "allContainedComments", // все комментарии в поддереве ноды + "childNodes", // все дети ноды без имен полей + "parentNode" // родительская нода + ).contains(name)) { + continue; + } + + printNonNodeValue(name, property.getValue(node)); + } +} + +private static void printNonNodeValue(String name, Object value) { + switch (value) { + case null -> { + return; + } + case Optional optionalValue -> { + optionalValue.ifPresent(innerValue -> printNonNodeValue(name, innerValue)); + return; + } + case NodeList values -> { + for (int i = 1; i <= values.size(); i++) { + printNonNodeValue(name + "[" + i + "]", values.get(i - 1)); + } + return; + } + case Node node -> { + return; + } + default -> { + } + } + + Class valueClass = value instanceof Enum enumValue + ? enumValue.getDeclaringClass() + : value.getClass(); + + String className = valueClass.getCanonicalName(); + // className может не быть у анонимных, лямбда, local классов + if (className == null) { + className = valueClass.getName().replace('$', '.'); + } + + String enumConstant = value instanceof Enum enumValue + ? "." + enumValue.name() + : ""; + + System.out.println(name + " -> " + className + enumConstant + " = " + value); +} +``` + +Пример использования: +```java +String code = "public class Sample{public int one(){if (a == b) return 1;}}"; + +CompilationUnit compilationUnit = StaticJavaParser.parse(code); +BinaryExpr binaryExpr = compilationUnit.findFirst(BinaryExpr.class).orElseThrow(); +printNonNodeProperties(binaryExpr); +``` + +Вывод будет таким: + +```text +operator -> com.github.javaparser.ast.expr.BinaryExpr.Operator.EQUALS = EQUALS +``` + +### Пример написания правила для таких конструкций +Определяем название: +```java +import com.github.javaparser.ast.Modifier; + +String code = "final class Empty{}"; + +CompilationUnit compilationUnit = StaticJavaParser.parse(code); +Modifier modifier = compilationUnit.findFirst(Modifier.class).orElseThrow(); +printNonNodeProperties(modifier); // keyword -> com.github.javaparser.ast.Modifier.Keyword.FINAL = FINAL +``` + +В DSL это все равно можно вынести в placeholder: +```ebnf + ::= Modifier(keyword=) // будет писать keyword на отдельной строке + => nl indent dedent nl; // с 1 табом отступа +``` # Итог ## Быстрый шаблон для нового правила 1. Найти в AST: - ```text SomeNode ``` -2. Посмотреть properties: - +2. Посмотреть properties этой ноды: ```text -left -operator -right +left -> a +operator -> PLUS +right -> b ``` -3. Написать правило: +3. Для каждого property понимаем, что там лежит: +```java +public class Example { + void example(Node node) { + for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels()) { + String name = property.getName(); + + if (List.of( + "metaModel", "range", "tokenRange", "parsed", + "comment", "orphanComments", "allContainedComments", + "childNodes", "parentNode" + ).contains(name)) { + continue; + } + + Object value = property.getValue(node); + + if (value == null) { + System.out.println(name + " -> null"); + } else if (value instanceof Node childNode) { + System.out.println(name + + " -> Node " + + childNode.getClass().getSimpleName() + + " = " + + childNode); + } else { + Class valueClass = value instanceof Enum enumValue + ? enumValue.getDeclaringClass() + : value.getClass(); + + String className = valueClass.getCanonicalName(); + if (className == null) { + className = valueClass.getName().replace('$', '.'); + } + + String enumConstant = value instanceof Enum enumValue + ? "." + enumValue.name() + : ""; -```ebnf - ::= SomeNode(left=, operator=, right=) - => sp sp ; + System.out.println(name + + " -> " + + className + + enumConstant + + " = " + + value); + } + } + } +} +``` +4. Например для BinaryExpr(a + b) будет: +```text +left -> Node NameExpr = a +operator -> com.github.javaparser.ast.expr.BinaryExpr.Operator.PLUS = PLUS +right -> Node NameExpr = b +``` +5. Если property содержит Node, выносим его в placeholder. Если property содержит не Node, например enum для operator/keyword/type, его тоже можно вынести в placeholder. +6. Пример правила: +```text + ::= SomeNode(left=, operator=, right=) + => sp sp ; ``` \ No newline at end of file From c8fef30ed70049a1eef9d52ab556eae4193e0f21 Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 8 May 2026 22:24:28 +0300 Subject: [PATCH 29/29] =?UTF-8?q?=D0=9F=D0=BE=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HowToWriteRules.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HowToWriteRules.md b/HowToWriteRules.md index 8dbd78f..7ff535c 100644 --- a/HowToWriteRules.md +++ b/HowToWriteRules.md @@ -84,7 +84,7 @@ for (PropertyMetaModel property : node.getMetaModel().getAllPropertyMetaModels() "metaModel", // описание модели самой ноды "range", // позиция в исходном файле "tokenRange", // диапазон токенов - "parsed", // получена ли нода парсером или создана програмно + "parsed", // получена ли нода парсером или создана программно "comment", // комментарий, прикрепленный к ноде "orphanComments", // комментарий, который был рядом с нодой "allContainedComments", // все комментарии в поддереве ноды @@ -235,7 +235,7 @@ private static void printNonNodeProperties(Node node) { "metaModel", // описание модели самой ноды "range", // позиция в исходном файле "tokenRange", // диапазон токенов - "parsed", // получена ли нода парсером или создана програмно + "parsed", // получена ли нода парсером или создана программно "comment", // комментарий, прикрепленный к ноде "orphanComments", // комментарий, который был рядом с нодой "allContainedComments", // все комментарии в поддереве ноды