Fixer design

dockwise fix is deliberately conservative: it only applies rewrites whose effect on the built image is understood, and leaves everything else to the linter.

Mechanics

fixer.fix_text runs a list of passes in a loop until a full round produces no edit. Each pass receives a freshly parsed Dockerfile and returns edits (replace an inclusive line range with new text). Edits within a pass never overlap and are applied bottom-up; the text is then re-parsed for the next pass. This keeps each pass simple (it never has to track shifted line numbers) and makes idempotency easy to test: a second run finds nothing to do.

Formatting is preserved wherever possible. Flag insertion edits the original instruction text with a regex scoped to a single command (up to the next &&, ; or |), so multi-line package lists stay as written:

RUN apt-get update && apt-get install -y \
    curl \
    git && rm -rf /var/lib/apt/lists/*

becomes

RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    git && rm -rf /var/lib/apt/lists/*

Passes

Pass Rules Behaviour
flags DW005 DW009 DW010 DW032 DW033 --no-install-recommends, -y (apt and yum/dnf/microdnf), --no-cache-dir (skipped when PIP_NO_CACHE_DIR is set or a cache mount is used), apk add --no-cache
merge apt DW008 Adjacent plain RUNs that run apt-get update/install (or only clean the lists) become one RUN joined with && \. Only blank lines may separate them; a comment between them stops the merge. RUNs with flags such as --mount are never merged.
apt cleanup DW006 Appends rm -rf /var/lib/apt/lists/* after the last install; a cleanup that sits before a later install is removed and re-added at the end
dependency order DW024 See below
JSON form DW023 DW031 CMD and ENTRYPOINT from shell form to exec form
misc DW014 DW039 ADD to COPY (not for URLs, archives that ADD would extract, or --checksum), MAINTAINER to LABEL org.opencontainers.image.authors

Worked example of the apt passes:

RUN apt-get update
RUN apt-get install -y curl git

becomes (flags pass, merge pass, cleanup pass in that order, then a second round that finds nothing):

RUN apt-get update && \
    apt-get install -y --no-install-recommends curl git && \
    rm -rf /var/lib/apt/lists/*

Exec-form conversion

CMD python app.py --port 8000 becomes CMD ["python", "app.py", "--port", "8000"]. The conversion is refused when the text contains any shell feature: variables ($), pipes and operators (|, &, ;), redirects, globs, ~, #, backslashes, a leading VAR=value, or builtins like cd and echo, because exec form would change behaviour. Quoted text is stripped before this check, so CMD run "a b" converts. A leading exec is dropped since exec form already runs the program directly as PID 1.

Moving the lockfile COPY before the install

Pattern: COPY . <dest> followed (possibly after ENV/ARG/LABEL) by a RUN that consists only of a dependency install. The rewrite is

COPY . /app              =>    COPY package.json package-lock.json /app/
RUN npm ci                     RUN npm ci
                               COPY . /app

All of these safety conditions must hold:

  1. The RUN is a plain shell-form RUN whose commands are the install plus only rm, mkdir or <tool> cache .... RUN npm ci && npm run build is not moved, because the build reads the source.
  2. The install is source independent: pip install -r file yes, pip install -e . or pip install . no; poetry install only with --no-root (otherwise poetry needs the project package); requirement file paths must be relative and free of ...
  3. No WORKDIR or USER sits between the COPY and the RUN (they would change what the paths mean).
  4. If the build context is known and a required manifest does not exist, nothing is changed. Existing optional lockfiles are copied by name; without context knowledge the primary lockfile is copied with a trailing * (package-lock.json*), which Docker accepts as long as another source (package.json) matches.
  5. --chown and other COPY flags are carried over, and the destination directory is kept (subdirectories of requirement files are mirrored).

After the rewrite the install no longer directly follows COPY . ., so neither the rule nor the pass fires again.

What it will not do

It never edits FROM, USER, secrets, curl | sh, package versions or .dockerignore, and it does not split a single stage into multiple stages. Those findings stay in the lint report because a correct fix needs knowledge only the author has.