Skip to content

Lint with oida, at zero baseline - #331

Merged
3lvis merged 12 commits into
masterfrom
ci/oida
Sep 16, 2026
Merged

3lvis merged 12 commits into
masterfrom
ci/oida

Conversation

@3lvis

@3lvis 3lvis commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Adopts oida as this repository's linter, with no baseline: the tree is kept at zero and any violation fails.

bin/lint fetches the pinned release, checksums it against .oida-checksum-<platform>, asserts the version after unzipping, and runs it. .oida.yml carries the rules. The commit hook calls bin/lint --staged, and CI calls bin/lint --check on ubuntu-latest in swift:6.3.2-noble — linting needs no Xcode, and that is the shape tienda-ios already runs. The macOS runner stays for build, test and the warnings gate.

One tool formats now. oida hands the corrected files to swift-format, so the standalone swift-format check job and the swift-format pre-commit hook were running it a second time; both are gone. .swift-format stays — swift-format still owns every indent, reached through oida, which is what keeps the tree and ⇧⌃I agreeing.

Where it stands

438 violations at the start, 9 now, suite 180/180 green at every commit and bin/lint --fix idempotent.

The nine are all no_single_use_void_functions, and they want a decision rather than an edit:

  • Three have callers in another fileCacheStore.storeData, CacheStore.sweepExpired, Networking+Private.cancelRequest. 0.15.0's note says a call from another file is an interface across a layer that the rule leaves alone; the shipped rule flags them, and its message says only "called from exactly one place in the app".
  • Networking.removeContinuation cannot be inlined at all. Its one caller is an AsyncStream termination handler, and putting the statement where it runs fails to compile: actor-isolated property 'streamContinuations' can not be mutated from a nonisolated context. The function is the isolation hop.
  • Five are genuinely same-file and the rule is right about them.

What the adoption changed

the --fix reformat, taken as one commit 438 → 278
XCTFail messages, Data(_.utf8), type names → 238
MARK banners, doc-comment placement, spacing → 220
force unwraps and casts, each on its own terms → 206
FakeRequest matching split into three steps → 195
oida 0.15.0, and lint moves to Linux → 48
the documents say what is → 9
oida 0.16.0, and 25 dead rule names go → 9

Four oida issues, which is the point of adopting it

All four are closed, and 0.15.0 and 0.16.0 are what closed them.

  • #19multiline_call_arguments: --fix cleared 158 of 283 and settled. One of its three reasons was corrected zero times out of 37.
  • #20multiline_parameters and opening_brace admitted no legal shape for a two-parameter signature longer than lineLength. 18 sites. 0.16.0's ignore_multiline_function_signatures is the answer.
  • #21no_single_use_void_functions flagged public func clearCache(), which the README documents as the consumer API.
  • #34--fix wrote 15 trailing commas that --check then rejected, stably. swift-format adds them by default and oida's trailing_comma forbade them. 0.16.0 deletes the rule: layout is the formatter's, so multiElementCollectionTrailingCommas decides alone.

Worth a look in review

Image.find traps with the name it looked for rather than force-unwrapping, on the grounds that a bundled asset is a packaging fault and never a caller's. Two generic-erasure casts keep a scoped oida:disable with the guard written next to them. And the prose pass rewrote 39 lines across README, AGENTS and LEARNINGS so they say what is — "A file outside the main bundle takes the bundle parameter" rather than "If your file is not located in the main bundle you have to specify using the bundle parameters".

oida (github.com/kolonialno/oida-swift) is our own linter: fifteen rules decide
the shape of every argument list, and the ones this repository opts into are in
.oida.yml. bin/lint fetches the pinned release, checksums it and runs it, then
hands the files to swift-format, which owns every indent.

There is no baseline. A standing violation would be silent under one, and this
repository adopted oida to find what it says rather than to record that it once
said something — so the tree goes to zero and stays there.

This commit is the mechanical half: what --fix produces, 438 violations down to
278. The suite is 180/180 green against go-httpbin, so the reformat changed
shape and nothing else. The remainder is hand work, and the gate goes on once
it is at zero.
bin/lint already hands the corrected files to swift-format, so the standalone
swift-format CI job and the swift-format pre-commit hook were running it a
second time. The hook now calls bin/lint --staged and CI calls bin/lint --check.

.swift-format stays: swift-format still owns every indent, it is just reached
through oida now, which is what keeps the tree and Xcode's Format File agreeing.
XCTFail() with no message tells whoever reads the failure nothing the test name
did not already say, so all seventeen now name the expectation: "a GET to a path
that does not exist must fail", "the image should come back from the file cache".

The rest is mechanical. Trailing commas dropped from collection literals. Two
force-unwrapped `"...".data(using: .utf8)!` become `Data("...".utf8)`, which
cannot fail and so needs no unwrap. `arc4random()` becomes
`UInt32.random(in: .min ... .max)`. Two test classes lose the underscore their
filename put in their name.

438 violations down to 238, suite 180/180 green.
Eleven `// MARK: -` banners named what the declarations under them already
named, so they are gone. A doc comment that sat above two ordinary comments
moves down onto the declaration it documents, where it is read as one.

The rest is spacing: a stored property now stands off the function under it.

238 violations down to 220, suite 180/180 green.
…ndling

Seventeen force unwraps and three force casts, each taken on its own terms.

Handled, because the value can genuinely be absent: a macOS image with no TIFF
representation now returns nil, which the signature already allowed; a folder
name that will not parse as a URL throws or returns nil rather than trapping;
five `"...".data(using: .utf8)!` become `Data("...".utf8)`, which cannot fail.

Stated, because the absence would be our fault rather than a caller's: a bundled
image that is missing is a packaging fault, so Image.find says so and traps with
the name it looked for. Two justified with a scoped disable: the URL reaching the
HTTPURLResponse convenience initializer has already been parsed by composedURL,
and the three generic casts are each guarded by the type test above them.

In tests a nil is a failed test, so those take XCTUnwrap.

220 violations down to 206, suite 180/180 green.
… three steps

FakeRequest.find carried an exact lookup and the templated-path walk in one body,
at complexity 14. It is now three: find tries the exact path then hands over,
templated walks the candidates, and captures answers what one placeholder path
took from a lookup — or nil where the two describe different paths. The body
substitution both halves needed is String.replacing(_:).

A `for` whose body was one `if` takes a `where` clause.

no_single_use_void_functions is left out of the rule set, with the reason in
.oida.yml: nine of the ten it finds here are a library's own seams and one is
documented public API, whose callers are in other people's projects
(kolonialno/oida-swift#21). Inlining the tenth does not even compile — it is the
hop that makes an actor-isolated write legal from a nonisolated closure.

206 violations down to 195, suite 180/180 green.
0.15.0 answers all three issues this adoption raised. The 149 violations that
had no fix in 0.14.0 are gone: multiline_call_arguments and multiline_conditions
correct now that width is the formatter's alone, and opening_brace takes
ignore_multiline_function_signatures, which is the shape a wrapped signature
could never satisfy before (#19, #20). no_single_use_void_functions no longer
flags public clearCache, whose callers are in other people's projects (#21), so
the rule comes back into the set.

The configuration loses what 0.15.0 deleted: requires_single_line, the eight
count rules, nesting and cyclomatic_complexity. Naming a rule that no longer
exists is naming nothing.

Linting needs no Xcode, so its job moves to ubuntu-latest in swift:6.3.2-noble,
the shape tienda-ios already runs. The toolchain image is what makes Linux work
at all: oida's SourceKit rules and its swift-format handoff both resolve through
it. The macOS runner stays for build, test and the warnings gate.

195 violations down to 48, suite 180/180 green.
Thirty-nine places where the prose said what something was rather than what it
is. A few read better for it: "If your file is not located in the main bundle you
have to specify using the bundle parameters" is now "A file outside the main
bundle takes the bundle parameter"; "makes no assumption about the error body's
shape" is "treats the error body as opaque"; the `.ephemeral` paragraph, carried
in from Apple's own documentation, says what it keeps in RAM instead of what it
declines to write.

Two uses of "simply" go, which was the whole of document_avoids_retired_words.

Every relative link still resolves.
swift-format adds a trailing comma to a multi-line collection literal by default
and oida's trailing_comma forbids one, so bin/lint --fix wrote fifteen commas
that bin/lint --check then rejected — stably, since a second --fix changed
nothing. tienda-ios has always set this key, which is why it reports none.

multiElementCollectionTrailingCommas: false is the half that makes both
directions agree; mandatory_comma: true alone leaves seven, because the
formatter adds the comma in most multi-line literals rather than all of them.

Filed as kolonialno/oida-swift#34. --fix is idempotent now.
0.16.0 takes 209 rules to 89, and 25 of the names this configuration carried are
among them — the SwiftLint opt-in menu, the whitespace rules swift-format
already reports, and the preferences that pick one of two correct spellings.
trailing_comma goes with them, which closes #34: layout is the formatter's, so
multiElementCollectionTrailingCommas decides alone and oida holds no opinion to
disagree with.

file_header and force_unwrapping left too. The scoped disable on the
HTTPURLResponse initializer goes with force_unwrapping, and its comment stays:
why that unwrap is safe is worth reading whether or not a rule asks.

--fix is idempotent, suite 180/180 green, and the tree is at 9 —
no_single_use_void_functions, all of it.
appendToLogFile had one caller, in the same file, as its last statement — the
jump bought the reader nothing and its guards read the same inside record.

The other two same-file cases stay. removeContinuation is an actor-isolation hop
whose caller is a nonisolated termination handler, and inlining it fails to
compile. logCompletion is fifty-five lines with three early exits, called from a
function that carries on afterwards; folding it turns those exits into nested
conditionals and makes complete do three jobs.

Suite 180/180 green.
Each carries a scoped disable and the reason above it. Six are seams: the cancel
path reached from the verbs, and the cache store's set, write, clear and sweep,
each reached from Networking and three of them driven directly by tests. Folding
any of them pushes a store's internals into its consumer.

Two are same-file and still stay. logCompletion is fifty lines of logging behind
a name, with three early exits the caller must never inherit — returning from
complete would skip emit and the result. removeContinuation is the hop that makes
an actor-isolated write legal from a nonisolated termination handler, and
inlining it fails to compile.

clear() takes a block disable rather than :next, so its doc comment stays on the
declaration it documents.

bin/lint --check is clean, --fix is idempotent, suite 180/180 green. The gate is
hard from here: any new violation fails.
@3lvis
3lvis marked this pull request as ready for review September 16, 2026 19:55
@3lvis
3lvis merged commit 48b3e95 into master Sep 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant