Skip to content

Commit 4f0e28f

Browse files
committed
feat: support lowercased instruction names
1 parent 30798a2 commit 4f0e28f

File tree

5 files changed

+104
-29
lines changed

5 files changed

+104
-29
lines changed

src/main/java/com/github/jimschubert/rewrite/docker/internal/DockerfileParser.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.github.jimschubert.rewrite.docker.internal;
1616

1717
import com.github.jimschubert.rewrite.docker.tree.Docker;
18+
import com.github.jimschubert.rewrite.docker.tree.InstructionName;
1819
import com.github.jimschubert.rewrite.docker.tree.Space;
1920
import org.openrewrite.Tree;
2021
import org.openrewrite.marker.Markers;
@@ -93,7 +94,7 @@ public Docker.Document parse(InputStream input) {
9394

9495
String instructionName = peekInstruction(line);
9596
if (state.isContinuation()
96-
&& "HEALTHCHECK".equals(instructionType)
97+
&& "HEALTHCHECK".equalsIgnoreCase(instructionType)
9798
&& "CMD".equalsIgnoreCase(instructionName)) {
9899
// if we are in a HEALTHCHECK and the next word is CMD, we need to treat this as a continuation
99100
// of the previous instruction, not a new one.
@@ -134,9 +135,13 @@ public Docker.Document parse(InputStream input) {
134135
eol = Space.EMPTY;
135136
}
136137

137-
Docker.Instruction instr = parseInstruction();
138+
Docker.Instruction instr = registry.getParserFor(instructionType).parse(instruction.toString(), state);
138139
if (instr != null) {
139140
instr = instr.withEol(eol);
141+
// if instructionType not upperCase, store the original casing in maker
142+
if (!instructionType.equals(instructionType.toUpperCase())) {
143+
instr = instr.withMarkers(instr.getMarkers().add(new InstructionName(Tree.randomId(), instructionType)));
144+
}
140145
}
141146
currentInstructions.add(instr);
142147
if (instr instanceof Docker.From) {
@@ -166,15 +171,6 @@ private void reset() {
166171
state.reset();
167172
}
168173

169-
/**
170-
* Parse the current instruction into an LST node.
171-
*
172-
* @return The parsed instruction as a {@link Docker.Instruction}.
173-
*/
174-
private Docker.Instruction parseInstruction() {
175-
return registry.getParserFor(instructionType).parse(instruction.toString(), state);
176-
}
177-
178174
/**
179175
* Handle heredoc syntax in the line. This is used to handle the case where the line contains heredoc syntax.
180176
* The heredoc syntax is removed from the line and the heredoc content is stored in the parser state.

src/main/java/com/github/jimschubert/rewrite/docker/tree/DockerfilePrinter.java

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,20 @@ public Docker visitStage(Docker.Stage stage, PrintOutputCapture<P> p) {
105105
return stage;
106106
}
107107

108+
private void instructionName(Docker.Instruction instruction, PrintOutputCapture<P> p) {
109+
String defaultName = instruction.getClass().getSimpleName();
110+
p.append(
111+
instruction.getMarkers().findFirst(InstructionName.class)
112+
.filter(f -> f.getName().equalsIgnoreCase(defaultName))
113+
.map(InstructionName::getName)
114+
.orElseGet(defaultName::toUpperCase)
115+
);
116+
}
117+
108118
@Override
109119
public Docker visitFrom(Docker.From from, PrintOutputCapture<P> p) {
110120
beforeSyntax(from, p);
111-
p.append("FROM");
121+
instructionName(from, p);
112122
if (from.getPlatform() != null && !StringUtils.isBlank(from.getPlatform().getText())) {
113123
visitSpace(from.getPlatform().getPrefix(), p);
114124
if (!from.getPlatform().getText().startsWith("--")) {
@@ -173,7 +183,7 @@ public Docker visitDirective(Docker.Directive directive, PrintOutputCapture<P> p
173183
@Override
174184
public Docker visitRun(Docker.Run run, PrintOutputCapture<P> p) {
175185
beforeSyntax(run, p);
176-
p.append("RUN");
186+
instructionName(run, p);
177187
if (run.getOptions() != null) {
178188
run.getOptions().forEach(o -> {
179189
visitOption(o, p);
@@ -193,7 +203,7 @@ public Docker visitRun(Docker.Run run, PrintOutputCapture<P> p) {
193203
@Override
194204
public Docker visitCmd(Docker.Cmd cmd, PrintOutputCapture<P> p) {
195205
beforeSyntax(cmd, p);
196-
p.append("CMD");
206+
instructionName(cmd, p);
197207
Form form = cmd.getForm();
198208
if (form == null) {
199209
form = Form.EXEC;
@@ -299,7 +309,7 @@ private Docker.KeyArgs visitKeyArgs(Docker.KeyArgs keyArgs, PrintOutputCapture<P
299309
@Override
300310
public Docker visitLabel(Docker.Label label, PrintOutputCapture<P> p) {
301311
beforeSyntax(label, p);
302-
p.append("LABEL");
312+
instructionName(label, p);
303313
for (DockerRightPadded<Docker.KeyArgs> padded : label.getArgs()) {
304314
visitKeyArgs(padded.getElement(), p);
305315
visitSpace(padded.getAfter(), p);
@@ -311,7 +321,7 @@ public Docker visitLabel(Docker.Label label, PrintOutputCapture<P> p) {
311321
@Override
312322
public Docker visitMaintainer(Docker.Maintainer maintainer, PrintOutputCapture<P> p) {
313323
beforeSyntax(maintainer, p);
314-
p.append("MAINTAINER");
324+
instructionName(maintainer, p);
315325
visitLiteral(maintainer.getName(), p);
316326
afterSyntax(maintainer, p);
317327
return maintainer;
@@ -320,7 +330,7 @@ public Docker visitMaintainer(Docker.Maintainer maintainer, PrintOutputCapture<P
320330
@Override
321331
public Docker visitExpose(Docker.Expose expose, PrintOutputCapture<P> p) {
322332
beforeSyntax(expose, p);
323-
p.append("EXPOSE");
333+
instructionName(expose, p);
324334
for (int i = 0; i < expose.getPorts().size(); i++) {
325335
DockerRightPadded<Docker.Port> padded = expose.getPorts().get(i);
326336
Docker.Port port = padded.getElement();
@@ -347,7 +357,7 @@ private void visitQuoting(Quoting quoting, PrintOutputCapture<P> p) {
347357
@Override
348358
public Docker visitEnv(Docker.Env env, PrintOutputCapture<P> p) {
349359
beforeSyntax(env, p);
350-
p.append("ENV");
360+
instructionName(env, p);
351361

352362
for (DockerRightPadded<Docker.KeyArgs> padded : env.getArgs()) {
353363
Docker.KeyArgs kvp = padded.getElement();
@@ -374,7 +384,7 @@ public Docker visitEnv(Docker.Env env, PrintOutputCapture<P> p) {
374384
@Override
375385
public Docker visitAdd(Docker.Add add, PrintOutputCapture<P> p) {
376386
beforeSyntax(add, p);
377-
p.append("ADD");
387+
instructionName(add, p);
378388

379389
if (add.getOptions() != null) {
380390
add.getOptions().forEach(o -> {
@@ -397,7 +407,7 @@ public Docker visitAdd(Docker.Add add, PrintOutputCapture<P> p) {
397407
@Override
398408
public Docker visitCopy(Docker.Copy copy, PrintOutputCapture<P> p) {
399409
beforeSyntax(copy, p);
400-
p.append("COPY");
410+
instructionName(copy, p);
401411

402412
if (copy.getOptions() != null) {
403413
copy.getOptions().forEach(o -> {
@@ -420,7 +430,7 @@ public Docker visitCopy(Docker.Copy copy, PrintOutputCapture<P> p) {
420430
@Override
421431
public Docker visitEntrypoint(Docker.Entrypoint entrypoint, PrintOutputCapture<P> p) {
422432
beforeSyntax(entrypoint, p);
423-
p.append("ENTRYPOINT");
433+
instructionName(entrypoint, p);
424434

425435
if (entrypoint.getForm() == Form.EXEC) {
426436
Space before = entrypoint.getExecFormPrefix();
@@ -460,7 +470,7 @@ public Docker visitEntrypoint(Docker.Entrypoint entrypoint, PrintOutputCapture<P
460470
@Override
461471
public Docker visitVolume(Docker.Volume volume, PrintOutputCapture<P> p) {
462472
beforeSyntax(volume, p);
463-
p.append("VOLUME");
473+
instructionName(volume, p);
464474

465475
if (volume.getForm() == Form.EXEC) {
466476
Space before = volume.getExecFormPrefix();
@@ -499,7 +509,7 @@ public Docker visitVolume(Docker.Volume volume, PrintOutputCapture<P> p) {
499509
@Override
500510
public Docker visitUser(Docker.User user, PrintOutputCapture<P> p) {
501511
beforeSyntax(user, p);
502-
p.append("USER");
512+
instructionName(user, p);
503513
visitSpace(user.getUsername().getPrefix(), p);
504514
p.append(user.getUsername().getText());
505515
visitSpace(user.getUsername().getTrailing(), p);
@@ -516,7 +526,7 @@ public Docker visitUser(Docker.User user, PrintOutputCapture<P> p) {
516526
@Override
517527
public Docker visitWorkdir(Docker.Workdir workdir, PrintOutputCapture<P> p) {
518528
beforeSyntax(workdir, p);
519-
p.append("WORKDIR");
529+
instructionName(workdir, p);
520530
visitLiteral(workdir.getPath(), p);
521531
afterSyntax(workdir, p);
522532
return workdir;
@@ -525,7 +535,7 @@ public Docker visitWorkdir(Docker.Workdir workdir, PrintOutputCapture<P> p) {
525535
@Override
526536
public Docker visitArg(Docker.Arg arg, PrintOutputCapture<P> p) {
527537
beforeSyntax(arg, p);
528-
p.append("ARG");
538+
instructionName(arg, p);
529539
arg.getArgs().forEach(padded -> {
530540
Docker.KeyArgs args = padded.getElement();
531541
if (args.getValue().getText() != null) {
@@ -544,7 +554,8 @@ public Docker visitArg(Docker.Arg arg, PrintOutputCapture<P> p) {
544554
@Override
545555
public Docker visitOnBuild(Docker.OnBuild onBuild, PrintOutputCapture<P> p) {
546556
beforeSyntax(onBuild, p);
547-
p.append("ONBUILD ");
557+
instructionName(onBuild, p);
558+
p.append(" ");
548559
visit(onBuild.getInstruction(), p);
549560
afterSyntax(onBuild, p);
550561
return onBuild;
@@ -553,7 +564,7 @@ public Docker visitOnBuild(Docker.OnBuild onBuild, PrintOutputCapture<P> p) {
553564
@Override
554565
public Docker visitStopSignal(Docker.StopSignal stopSignal, PrintOutputCapture<P> p) {
555566
beforeSyntax(stopSignal, p);
556-
p.append("STOPSIGNAL");
567+
instructionName(stopSignal, p);
557568
visitSpace(stopSignal.getPrefix(), p);
558569
visitLiteral(stopSignal.getSignal(), p);
559570
afterSyntax(stopSignal, p);
@@ -563,7 +574,7 @@ public Docker visitStopSignal(Docker.StopSignal stopSignal, PrintOutputCapture<P
563574
@Override
564575
public Docker visitHealthcheck(Docker.Healthcheck healthcheck, PrintOutputCapture<P> p) {
565576
beforeSyntax(healthcheck, p);
566-
p.append("HEALTHCHECK");
577+
instructionName(healthcheck, p);
567578

568579
if (healthcheck.getType() == Docker.Healthcheck.Type.NONE) {
569580
p.append(" NONE");
@@ -612,7 +623,7 @@ public Docker visitHealthcheck(Docker.Healthcheck healthcheck, PrintOutputCaptur
612623
@Override
613624
public Docker visitShell(Docker.Shell shell, PrintOutputCapture<P> p) {
614625
beforeSyntax(shell, p);
615-
p.append("SHELL");
626+
instructionName(shell, p);
616627
if ("".equals(shell.getExecFormPrefix().getWhitespace())) {
617628
p.append(" ");
618629
} else {
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2025 Jim Schubert
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package com.github.jimschubert.rewrite.docker.tree;
16+
17+
import lombok.Value;
18+
import lombok.With;
19+
import org.openrewrite.marker.Marker;
20+
21+
import java.util.UUID;
22+
23+
@Value
24+
@With
25+
public class InstructionName implements Marker {
26+
UUID id;
27+
String name;
28+
}

src/test/java/com/github/jimschubert/rewrite/docker/internal/DockerfileParserTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,22 @@ void testArgNoAssignment() {
125125
assertRightPaddedArg(args.get(0), Quoting.UNQUOTED, " ", "foo", false, null, "");
126126
}
127127

128+
@Test
129+
void testArgNoAssignmentLowercased() {
130+
DockerfileParser parser = new DockerfileParser();
131+
Docker.Document doc = parser.parse(new ByteArrayInputStream("arg foo".getBytes(StandardCharsets.UTF_8)));
132+
133+
Docker.Stage stage = assertSingleStageWithChildCount(doc, 1);
134+
135+
Docker.Arg arg = (Docker.Arg) stage.getChildren().get(0);
136+
assertTrue(arg.getMarkers().findFirst(InstructionName.class).isPresent());
137+
138+
assertEquals(Space.EMPTY, arg.getPrefix());
139+
List<DockerRightPadded<Docker.KeyArgs>> args = arg.getArgs();
140+
141+
assertRightPaddedArg(args.get(0), Quoting.UNQUOTED, " ", "foo", false, null, "");
142+
}
143+
128144
@Test
129145
void testArgComplex() {
130146
DockerfileParser parser = new DockerfileParser();

src/test/java/com/github/jimschubert/rewrite/docker/tree/DockerfilePrinterTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,30 @@ void visitFrom() {
3535
assertEquals(expected, doc.print(new Cursor(null, new DockerfilePrinter<Integer>())));
3636
}
3737

38+
@Test
39+
void visitFromLowercased() {
40+
Docker.Document doc = Docker.Document.build(
41+
Docker.From.build("alpine:latest")
42+
.withEol(Space.EMPTY)
43+
.withMarkers(Markers.build(List.of(new InstructionName(Tree.randomId(), "from"))))
44+
).withEof(Space.EMPTY);
45+
46+
String expected = "from alpine:latest";
47+
assertEquals(expected, doc.print(new Cursor(null, new DockerfilePrinter<Integer>())));
48+
}
49+
50+
@Test
51+
void visitFromIgnoreInvalidInstructionName() {
52+
Docker.Document doc = Docker.Document.build(
53+
Docker.From.build("alpine:latest")
54+
.withEol(Space.EMPTY)
55+
.withMarkers(Markers.build(List.of(new InstructionName(Tree.randomId(), "apples"))))
56+
).withEof(Space.EMPTY);
57+
58+
String expected = "FROM alpine:latest";
59+
assertEquals(expected, doc.print(new Cursor(null, new DockerfilePrinter<Integer>())));
60+
}
61+
3862
@Test
3963
void visitFromFull() {
4064
Docker.Document doc = Docker.Document.build(

0 commit comments

Comments
 (0)