Skip to content

Commit

Permalink
Correct parsing for f-string string literal concatenation
Browse files Browse the repository at this point in the history
Fixes #91

Add support for f-string literal concatenation in the Python parser.

* **Py.java**
  - Add a new nested `StringLiteralConcatenation` type to handle string literal concatenation.
  - Implement `getType()` to return `JavaType.Primitive.String`.
  - Implement `setType()` to return `this`.

* **_parser_visitor.py**
  - Update the `visit_Constant` method to handle string literal concatenation combined with f-strings.
  - Add logic to produce `Py.StringLiteralConcatenation` nodes for string literal concatenation.
  - Update the `__map_fstring` method to handle the difficult piece of logic for f-string concatenation.

* **fstring_test.py**
  - Add tests to verify correct parsing of string literal concatenation combined with f-strings.
  - Add tests to verify correct parsing of f-string literal concatenation with comments.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/openrewrite/rewrite-python/issues/91?shareId=XXXX-XXXX-XXXX-XXXX).
  • Loading branch information
knutwannheden committed Oct 19, 2024
1 parent 86e6295 commit c210036
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 18 deletions.
82 changes: 82 additions & 0 deletions rewrite-python/src/main/java/org/openrewrite/python/tree/Py.java
Original file line number Diff line number Diff line change
Expand Up @@ -2253,4 +2253,86 @@ public Slice withStep(@Nullable JRightPadded<Expression> step) {
}
}

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = false)
@RequiredArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
final class StringLiteralConcatenation implements Py, Expression, TypedTree {
@Nullable
@NonFinal
transient WeakReference<Padding> padding;

@Getter
@With
@EqualsAndHashCode.Include
UUID id;

@Getter
@With
Space prefix;

@Getter
@With
Markers markers;

List<JRightPadded<Expression>> literals;

public List<Expression> getLiterals() {
return JRightPadded.getElements(literals);
}

public StringLiteralConcatenation withLiterals(List<Expression> literals) {
return getPadding().withLiterals(JRightPadded.withElements(this.literals, literals));
}

@Override
public JavaType getType() {
return JavaType.Primitive.String;
}

@Override
public <T extends J> T withType(@Nullable JavaType type) {
//noinspection unchecked
return (T) this;
}

@Override
public <P> J acceptPython(PythonVisitor<P> v, P p) {
return v.visitStringLiteralConcatenation(this, p);
}

@Override
@Transient
public CoordinateBuilder.Expression getCoordinates() {
return new CoordinateBuilder.Expression(this);
}

public Padding getPadding() {
Padding p;
if (this.padding == null) {
p = new Padding(this);
this.padding = new WeakReference<>(p);
} else {
p = this.padding.get();
if (p == null || p.t != this) {
p = new Padding(this);
this.padding = new WeakReference<>(p);
}
}
return p;
}

@RequiredArgsConstructor
public static class Padding {
private final StringLiteralConcatenation t;

public List<JRightPadded<Expression>> getLiterals() {
return t.literals;
}

public StringLiteralConcatenation withLiterals(List<JRightPadded<Expression>> literals) {
return t.literals == literals ? t : new StringLiteralConcatenation(t.id, t.prefix, t.markers, literals, t.type);
}
}
}
}
61 changes: 43 additions & 18 deletions rewrite/rewrite/python/_parser_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1294,15 +1294,31 @@ def visit_Constant(self, node):
else:
break

return j.Literal(
random_id(),
prefix,
Markers.EMPTY,
None if node.value is Ellipsis else node.value,
self._source[start:self._cursor],
None,
self.__map_type(node),
)
if isinstance(node.value, str) and '\n' in node.value:
return py.StringLiteralConcatenation(
random_id(),
prefix,
Markers.EMPTY,
[self.__pad_right(j.Literal(
random_id(),
Space.EMPTY,
Markers.EMPTY,
None if node.value is Ellipsis else node.value,
self._source[start:self._cursor],
None,
self.__map_type(node),
), Space.EMPTY)]
)
else:
return j.Literal(
random_id(),
prefix,
Markers.EMPTY,
None if node.value is Ellipsis else node.value,
self._source[start:self._cursor],
None,
self.__map_type(node),
)


def visit_Dict(self, node):
Expand Down Expand Up @@ -2123,6 +2139,7 @@ def __map_fstring(self, node: ast.JoinedStr, prefix: Space, tok: TokenInfo, toke

# tokenizer tokens: FSTRING_START, FSTRING_MIDDLE, OP, ..., OP, FSTRING_MIDDLE, FSTRING_END
parts = []
literals = []
for value in node.values:
if tok.type == token.OP and tok.string == '{':
if not isinstance(value, ast.FormattedValue):
Expand Down Expand Up @@ -2201,29 +2218,37 @@ def __map_fstring(self, node: ast.JoinedStr, prefix: Space, tok: TokenInfo, toke
self._cursor += len(tok.string) + (1 if tok.string.endswith('{') or tok.string.endswith('}') else 0)
if (tok := next(tokens)).type != token.FSTRING_MIDDLE:
break
parts.append(j.Literal(
literals.append(self.__pad_right(j.Literal(
random_id(),
Space.EMPTY,
Markers.EMPTY,
cast(ast.Constant, value).s,
self._source[save_cursor:self._cursor],
None,
self.__map_type(value),
))
), Space.EMPTY))

if consume_end_delim:
self._cursor += len(tok.string) # FSTRING_END token
tok = next(tokens)
elif tok.type == token.FSTRING_MIDDLE and len(tok.string) == 0:
tok = next(tokens)

return (py.FormattedString(
random_id(),
prefix,
Markers.EMPTY,
delimiter,
parts
), tok)
if literals:
return (py.StringLiteralConcatenation(
random_id(),
prefix,
Markers.EMPTY,
literals
), tok)
else:
return (py.FormattedString(
random_id(),
prefix,
Markers.EMPTY,
delimiter,
parts
), tok)

def __cursor_at(self, s: str):
return self._cursor < len(self._source) and (len(s) == 1 and self._source[self._cursor] == s or self._source.startswith(s, self._cursor))
29 changes: 29 additions & 0 deletions rewrite/tests/python/all/fstring_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,32 @@ def test_nested_fstring_with_format_value():
def test_adjoining_expressions():
# language=python
rewrite_run(python("""a = f'{1}{0}'"""))


def test_fstring_literal_concatenation():
# language=python
rewrite_run(
python(
"""
a = (
f"foo"
f"bar"
)
"""
)
)


def test_fstring_literal_concatenation_with_comments():
# language=python
rewrite_run(
python(
"""
a = (
f"foo"
# comment
f"bar"
)
"""
)
)

0 comments on commit c210036

Please sign in to comment.