Skip to content

Commit 8e2ef24

Browse files
committed
TOML: structured DottedKey AST type and path-lookup utilities
A dotted TOML key like `physical.color` was previously flattened into a single `Toml.Identifier` whose `name` was the joined string of all child tokens. That representation cannot distinguish `site."google.com"` (two segments, the second containing a literal dot) from `site.google.com` (three bare segments) — both became the string `site.google.com`. Recipes wanting to find or modify a value by logical key path each ended up doing ad-hoc traversal that handled only some of the equivalent authoring forms. Add `Toml.DottedKey implements TomlKey` with an ordered list of `Toml.Identifier` segments wrapped in `TomlRightPadded`. Each segment preserves its own prefix/source for round-tripping, and the right-padding holds the whitespace before the following dot. The dots themselves are emitted by the printer between segments rather than stored. `Toml.Table.name` widens from `TomlRightPadded<Toml.Identifier>` to `TomlRightPadded<TomlKey>` so headers can carry either shape. `TomlKey` gains a `getPath()` default returning the canonical list of unquoted segment names — singleton for a simple `Identifier`, N-element for a `DottedKey`. A `getName()` default returns those segments joined with `.`, matching the existing `Identifier.getName()` semantics so consumers that compare names as strings keep working unchanged. `TomlPaths` is a new static utility offering `findKeyValue` and `findTable` over a `Document`. The finder walks the document and matches a target path regardless of whether the document expressed it as a flat dotted key (`a.b.c.x = 1`), nested headers (`[a] [a.b] [a.b.c] x = 1`), `[a.b.c] x = 1`, `[a.b] c.x = 1`, or nested inline tables. Quoted segments containing literal dots are treated as one segment. Also: `TomlVisitor.visitTable` now visits the table name so subclasses that transform identifiers/dotted keys see headers as well as key-value keys; previously the name was silently skipped. `SemanticallyEqual.keyEquals` and `TomlPathMatcher` are simplified to use `getPath()` directly. `PythonDependencyParser.indexTables` is adjusted so dotted-header tables (e.g. `[tool.uv]`) keep being indexed.
1 parent 8909446 commit 8e2ef24

12 files changed

Lines changed: 481 additions & 64 deletions

File tree

rewrite-python/src/main/java/org/openrewrite/python/internal/PythonDependencyParser.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,8 @@ private static Map<String, Toml.Table> indexTables(Toml.Document doc) {
103103
for (TomlValue value : doc.getValues()) {
104104
if (value instanceof Toml.Table) {
105105
Toml.Table table = (Toml.Table) value;
106-
Toml.Identifier nameId = table.getName();
107-
if (nameId != null) {
108-
tables.put(nameId.getName(), table);
106+
if (table.getName() != null) {
107+
tables.put(table.getName().getName(), table);
109108
}
110109
}
111110
}

rewrite-toml/src/main/java/org/openrewrite/toml/SemanticallyEqual.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,21 @@ public Toml visitIdentifier(Toml.Identifier identifier, Toml other) {
175175
return null;
176176
}
177177

178+
@Override
179+
public Toml visitDottedKey(Toml.DottedKey dottedKey, Toml other) {
180+
if (dottedKey == other) {
181+
return null;
182+
}
183+
if (!(other instanceof Toml.DottedKey)) {
184+
areEqual = false;
185+
return null;
186+
}
187+
if (!dottedKey.getPath().equals(((Toml.DottedKey) other).getPath())) {
188+
areEqual = false;
189+
}
190+
return null;
191+
}
192+
178193
@Override
179194
public Toml visitEmpty(Toml.Empty empty, Toml other) {
180195
if (empty == other) {
@@ -214,18 +229,9 @@ private boolean keyEquals(@Nullable TomlKey key1, @Nullable TomlKey key2) {
214229
if (key1 == null || key2 == null) {
215230
return false;
216231
}
217-
218-
// Both keys must be of the same type
219-
if (key1.getClass() != key2.getClass()) {
220-
return false;
221-
}
222-
223-
// Compare identifier keys
224-
if (key1 instanceof Toml.Identifier && key2 instanceof Toml.Identifier) {
225-
return ((Toml.Identifier) key1).getName().equals(((Toml.Identifier) key2).getName());
226-
}
227-
228-
return false;
232+
// Same canonical path counts as equal regardless of whether the key
233+
// was authored as a simple key or a dotted key.
234+
return key1.getPath().equals(key2.getPath());
229235
}
230236
}
231237
}

rewrite-toml/src/main/java/org/openrewrite/toml/TomlIsoVisitor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ public Toml.Identifier visitIdentifier(Toml.Identifier identifier, P p) {
3939
return (Toml.Identifier) super.visitIdentifier(identifier, p);
4040
}
4141

42+
@Override
43+
public Toml.DottedKey visitDottedKey(Toml.DottedKey dottedKey, P p) {
44+
return (Toml.DottedKey) super.visitDottedKey(dottedKey, p);
45+
}
46+
4247
@Override
4348
public Toml.KeyValue visitKeyValue(Toml.KeyValue keyValue, P p) {
4449
return (Toml.KeyValue) super.visitKeyValue(keyValue, p);

rewrite-toml/src/main/java/org/openrewrite/toml/TomlPathMatcher.java

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import org.jspecify.annotations.Nullable;
1919
import org.openrewrite.Cursor;
2020
import org.openrewrite.toml.tree.Toml;
21-
import org.openrewrite.toml.tree.TomlKey;
2221

2322
import java.util.ArrayList;
2423
import java.util.List;
@@ -95,29 +94,20 @@ private List<String> buildPath(Cursor cursor) {
9594

9695
if (value instanceof Toml.KeyValue) {
9796
Toml.KeyValue kv = (Toml.KeyValue) value;
98-
TomlKey key = kv.getKey();
99-
if (key instanceof Toml.Identifier) {
100-
String keyName = ((Toml.Identifier) key).getName();
101-
Cursor parent = current.getParent();
102-
while (parent != null) {
103-
Object parentValue = parent.getValue();
104-
if (parentValue instanceof Toml.Table) {
105-
Toml.Table table = (Toml.Table) parentValue;
106-
if (table.getName() != null) {
107-
String tableName = table.getName().getName();
108-
// Split dotted names: [tool.poetry]
109-
String[] parts = tableName.split("\\.");
110-
for (int i = parts.length - 1; i >= 0; i--) {
111-
path.add(0, parts[i].trim());
112-
}
113-
}
114-
break;
97+
Cursor parent = current.getParent();
98+
while (parent != null) {
99+
Object parentValue = parent.getValue();
100+
if (parentValue instanceof Toml.Table) {
101+
Toml.Table table = (Toml.Table) parentValue;
102+
if (table.getName() != null) {
103+
path.addAll(table.getName().getPath());
115104
}
116-
parent = parent.getParent();
105+
break;
117106
}
118-
path.add(keyName);
119-
return path;
107+
parent = parent.getParent();
120108
}
109+
path.addAll(kv.getKey().getPath());
110+
return path;
121111
}
122112

123113
current = current.getParent();
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.toml;
17+
18+
import org.jspecify.annotations.Nullable;
19+
import org.openrewrite.toml.marker.ArrayTable;
20+
import org.openrewrite.toml.marker.InlineTable;
21+
import org.openrewrite.toml.tree.Toml;
22+
import org.openrewrite.toml.tree.TomlValue;
23+
24+
import java.util.List;
25+
26+
/**
27+
* Path-based lookup over a {@link Toml.Document}. Resolves a logical key path
28+
* (a list of unquoted segment names) to the AST node for that key, regardless
29+
* of whether the document expressed it as
30+
* <ul>
31+
* <li>a flat dotted key ({@code a.b.c.x = 1}),</li>
32+
* <li>nested table headers ({@code [a.b.c] x = 1}),</li>
33+
* <li>nested inline tables ({@code a = {b = {c = {x = 1}}}}), or</li>
34+
* <li>any combination of the above (e.g., {@code [a.b] c.x = 1}).</li>
35+
* </ul>
36+
*
37+
* <p>Quoted segments containing literal dots are treated as a single segment,
38+
* so {@code site."google.com"} resolves at path {@code ["site", "google.com"]}
39+
* (length 2), distinct from {@code site.google.com} which resolves at
40+
* {@code ["site", "google", "com"]} (length 3).
41+
*
42+
* <p>Array tables ({@code [[products]]}) are not searched: there is no way to
43+
* disambiguate which element of the array a path refers to.
44+
*/
45+
public final class TomlPaths {
46+
47+
private TomlPaths() {
48+
}
49+
50+
/**
51+
* Find the {@link Toml.KeyValue} at the given logical key path, or
52+
* {@code null} if no such key exists.
53+
*/
54+
public static Toml.@Nullable KeyValue findKeyValue(Toml.Document doc, List<String> path) {
55+
if (path.isEmpty()) {
56+
return null;
57+
}
58+
return findKeyValueIn(doc.getValues(), path);
59+
}
60+
61+
/**
62+
* Find a standard (non-array, non-inline) {@link Toml.Table} whose header
63+
* matches the given logical key path, or {@code null} if no such table
64+
* exists. Implicit tables defined only via dotted keys (e.g. {@code [a.b]}
65+
* implicitly defines {@code [a]}) are not returned — only tables that are
66+
* explicitly written as {@code [path]} are matched.
67+
*/
68+
public static Toml.@Nullable Table findTable(Toml.Document doc, List<String> path) {
69+
if (path.isEmpty()) {
70+
return null;
71+
}
72+
for (TomlValue value : doc.getValues()) {
73+
if (!(value instanceof Toml.Table)) {
74+
continue;
75+
}
76+
Toml.Table table = (Toml.Table) value;
77+
if (isStandardTable(table) && table.getName() != null && path.equals(table.getName().getPath())) {
78+
return table;
79+
}
80+
}
81+
return null;
82+
}
83+
84+
private static Toml.@Nullable KeyValue findKeyValueIn(List<? extends Toml> elements, List<String> targetSuffix) {
85+
for (Toml element : elements) {
86+
if (element instanceof Toml.KeyValue) {
87+
Toml.KeyValue kv = (Toml.KeyValue) element;
88+
List<String> kvPath = kv.getKey().getPath();
89+
if (kvPath.equals(targetSuffix)) {
90+
return kv;
91+
}
92+
if (kv.getValue() instanceof Toml.Table) {
93+
Toml.KeyValue found = recurseAtPrefix(kvPath, ((Toml.Table) kv.getValue()).getValues(), targetSuffix);
94+
if (found != null) {
95+
return found;
96+
}
97+
}
98+
} else if (element instanceof Toml.Table) {
99+
Toml.Table table = (Toml.Table) element;
100+
if (!isStandardTable(table) || table.getName() == null) {
101+
continue;
102+
}
103+
Toml.KeyValue found = recurseAtPrefix(table.getName().getPath(), table.getValues(), targetSuffix);
104+
if (found != null) {
105+
return found;
106+
}
107+
}
108+
}
109+
return null;
110+
}
111+
112+
private static Toml.@Nullable KeyValue recurseAtPrefix(List<String> prefix, List<? extends Toml> children, List<String> targetSuffix) {
113+
if (prefix.size() >= targetSuffix.size() || !targetSuffix.subList(0, prefix.size()).equals(prefix)) {
114+
return null;
115+
}
116+
return findKeyValueIn(children, targetSuffix.subList(prefix.size(), targetSuffix.size()));
117+
}
118+
119+
private static boolean isStandardTable(Toml.Table table) {
120+
return !table.getMarkers().findFirst(InlineTable.class).isPresent() &&
121+
!table.getMarkers().findFirst(ArrayTable.class).isPresent();
122+
}
123+
}

rewrite-toml/src/main/java/org/openrewrite/toml/TomlVisitor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.openrewrite.internal.ListUtils;
2323
import org.openrewrite.toml.tree.Space;
2424
import org.openrewrite.toml.tree.Toml;
25+
import org.openrewrite.toml.tree.TomlKey;
2526
import org.openrewrite.toml.tree.TomlRightPadded;
2627
import org.openrewrite.toml.tree.TomlValue;
2728

@@ -64,6 +65,13 @@ public Toml visitIdentifier(Toml.Identifier identifier, P p) {
6465
return i.withMarkers(visitMarkers(i.getMarkers(), p));
6566
}
6667

68+
public Toml visitDottedKey(Toml.DottedKey dottedKey, P p) {
69+
Toml.DottedKey d = dottedKey;
70+
d = d.withPrefix(visitSpace(d.getPrefix(), p));
71+
d = d.withMarkers(visitMarkers(d.getMarkers(), p));
72+
return d.getPadding().withNames(ListUtils.map(d.getPadding().getNames(), n -> visitRightPadded(n, p)));
73+
}
74+
6775
public Toml visitKeyValue(Toml.KeyValue keyValue, P p) {
6876
Toml.KeyValue kv = keyValue;
6977
kv = kv.withPrefix(visitSpace(kv.getPrefix(), p));
@@ -86,6 +94,10 @@ public Toml visitTable(Toml.Table table, P p) {
8694
Toml.Table t = table;
8795
t = t.withPrefix(visitSpace(t.getPrefix(), p));
8896
t = t.withMarkers(visitMarkers(t.getMarkers(), p));
97+
TomlRightPadded<TomlKey> name = t.getPadding().getName();
98+
if (name != null) {
99+
t = t.getPadding().withName(visitRightPadded(name, p));
100+
}
89101
return t.withValues(ListUtils.map(t.getValues(), v -> visit(v, p)));
90102
}
91103

rewrite-toml/src/main/java/org/openrewrite/toml/internal/TomlParserVisitor.java

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,11 @@ public Toml.Document visitDocument(TomlParser.DocumentContext ctx) {
128128
}
129129

130130
@Override
131-
public Toml.Identifier visitKey(TomlParser.KeyContext ctx) {
132-
return (Toml.Identifier) super.visitKey(ctx);
131+
public TomlKey visitKey(TomlParser.KeyContext ctx) {
132+
if (ctx.simpleKey() != null) {
133+
return visitSimpleKey(ctx.simpleKey());
134+
}
135+
return visitDottedKey(ctx.dottedKey());
133136
}
134137

135138
@Override
@@ -147,22 +150,16 @@ public Toml.Identifier visitSimpleKey(TomlParser.SimpleKeyContext ctx) {
147150
}
148151

149152
@Override
150-
public Toml.Identifier visitDottedKey(TomlParser.DottedKeyContext ctx) {
153+
public Toml.DottedKey visitDottedKey(TomlParser.DottedKeyContext ctx) {
151154
Space prefix = prefix(ctx);
152-
StringBuilder text = new StringBuilder();
153-
StringBuilder key = new StringBuilder();
154-
for (ParseTree child : ctx.children) {
155-
Space space = sourceBefore(child.getText());
156-
text.append(space.getWhitespace()).append(child.getText());
157-
key.append(child.getText());
155+
List<TomlParser.SimpleKeyContext> simpleKeys = ctx.simpleKey();
156+
List<TomlRightPadded<Toml.Identifier>> segments = new ArrayList<>(simpleKeys.size());
157+
for (int i = 0; i < simpleKeys.size(); i++) {
158+
Toml.Identifier segment = visitSimpleKey(simpleKeys.get(i));
159+
Space after = i < simpleKeys.size() - 1 ? sourceBefore(".") : Space.EMPTY;
160+
segments.add(TomlRightPadded.build(segment).withAfter(after));
158161
}
159-
return new Toml.Identifier(
160-
randomId(),
161-
prefix,
162-
Markers.EMPTY,
163-
text.toString(),
164-
key.toString()
165-
);
162+
return new Toml.DottedKey(randomId(), prefix, Markers.EMPTY, segments);
166163
}
167164

168165
/**
@@ -192,7 +189,7 @@ public Toml.KeyValue visitKeyValue(TomlParser.KeyValueContext ctx) {
192189
randomId(),
193190
prefix,
194191
Markers.EMPTY,
195-
TomlRightPadded.build((TomlKey) visitKey(c.key())).withAfter(sourceBefore("=")),
192+
TomlRightPadded.build(visitKey(c.key())).withAfter(sourceBefore("=")),
196193
visitValue(c.value())
197194
));
198195
}
@@ -413,8 +410,8 @@ public Toml visitInlineTable(TomlParser.InlineTableContext ctx) {
413410
public Toml visitStandardTable(TomlParser.StandardTableContext ctx) {
414411
return convert(ctx, (c, prefix) -> {
415412
sourceBefore("[");
416-
Toml.Identifier tableName = visitKey(c.key());
417-
TomlRightPadded<Toml.Identifier> nameRightPadded = TomlRightPadded.build(tableName).withAfter(sourceBefore("]"));
413+
TomlKey tableName = visitKey(c.key());
414+
TomlRightPadded<TomlKey> nameRightPadded = TomlRightPadded.build(tableName).withAfter(sourceBefore("]"));
418415

419416
List<TomlParser.KeyValueContext> values = c.keyValue();
420417
List<TomlRightPadded<Toml>> elements = new ArrayList<>();
@@ -436,8 +433,8 @@ public Toml visitStandardTable(TomlParser.StandardTableContext ctx) {
436433
public Toml visitArrayTable(TomlParser.ArrayTableContext ctx) {
437434
return convert(ctx, (c, prefix) -> {
438435
sourceBefore("[[");
439-
Toml.Identifier tableName = visitKey(c.key());
440-
TomlRightPadded<Toml.Identifier> nameRightPadded = TomlRightPadded.build(tableName).withAfter(sourceBefore("]]"));
436+
TomlKey tableName = visitKey(c.key());
437+
TomlRightPadded<TomlKey> nameRightPadded = TomlRightPadded.build(tableName).withAfter(sourceBefore("]]"));
441438

442439
List<TomlParser.KeyValueContext> values = c.keyValue();
443440
List<TomlRightPadded<Toml>> elements = new ArrayList<>();

rewrite-toml/src/main/java/org/openrewrite/toml/internal/TomlPrinter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ public Toml visitIdentifier(Toml.Identifier identifier, PrintOutputCapture<P> p)
6666
return identifier;
6767
}
6868

69+
@Override
70+
public Toml visitDottedKey(Toml.DottedKey dottedKey, PrintOutputCapture<P> p) {
71+
beforeSyntax(dottedKey, p);
72+
visitRightPadded(dottedKey.getPadding().getNames(), ".", p);
73+
afterSyntax(dottedKey, p);
74+
return dottedKey;
75+
}
76+
6977
@Override
7078
public Toml visitKeyValue(Toml.KeyValue keyValue, PrintOutputCapture<P> p) {
7179
beforeSyntax(keyValue, p);

0 commit comments

Comments
 (0)