diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index 40333d4d..c405afcf 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -26,15 +26,31 @@ concurrency: jobs: swiftlint: - name: SwiftLint (warning-only) + # No longer "warning-only". With a config in place the error-severity rules + # (force_try, force_cast, implicitly_unwrapped_optional) now BLOCK, because + # a red check that blocks nothing is what let 3,104 violations accumulate. + # The remaining ~33 are warning-severity and do not fail the job; zeroing + # them and adding --strict is the follow-up. + name: SwiftLint runs-on: macos-14 timeout-minutes: 10 - continue-on-error: true steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 - - name: Install SwiftLint - run: brew install swiftlint + # PINNED, for the same reason ruff is pinned in helper-ci.yml: an + # unpinned linter turns PRs red for code they never touched the moment + # upstream enables a rule. `brew install swiftlint` was unpinned here + # while ruff carried a paragraph explaining why that is unacceptable. + # Raising this is a deliberate task — bump it, run the lint, fix or + # allowlist what the new version finds, in a PR that does only that. + - name: Install SwiftLint (pinned) + run: | + set -euo pipefail + curl -fsSL -o /tmp/swiftlint.zip \ + https://github.com/realm/SwiftLint/releases/download/0.63.2/portable_swiftlint.zip + unzip -q -o /tmp/swiftlint.zip -d /tmp/swiftlint + sudo install -m 0755 /tmp/swiftlint/swiftlint /usr/local/bin/swiftlint + swiftlint version - name: Run SwiftLint on CLI Pulse Bar targets working-directory: CLI Pulse Bar diff --git a/CLI Pulse Bar/.swiftlint.yml b/CLI Pulse Bar/.swiftlint.yml new file mode 100644 index 00000000..92fa7d85 --- /dev/null +++ b/CLI Pulse Bar/.swiftlint.yml @@ -0,0 +1,66 @@ +# SwiftLint configuration — opt-in, not opt-out. +# +# WHY THIS FILE EXISTS +# -------------------- +# There was no config at all, so CI ran SwiftLint's DEFAULT rule set and +# reported 3,104 violations across 248 files, every run, for months. A check +# that is always red carries exactly as much information as one that is always +# green: none. It cost real time too — v1.49's PR had to verify by hand that +# its new files were clean, because the check itself could not say. +# +# The defaults were not wrong so much as not this project's. 1,734 of the +# 3,104 — 56% — were `identifier_name` objecting to `s`, `v`, `c`, `k`, `d` +# in closures and math. That is a house-style opinion this codebase has +# declined 1,734 times. Another ~700 were size and line-length rules, which +# measure "this file is big", something everyone already knows. +# +# So this is `only_rules`, an ALLOWLIST, rather than a long `disabled_rules` +# list. Two reasons: +# * it states what we believe instead of what we tolerate, and +# * a SwiftLint upgrade cannot silently add a new rule and turn every PR +# red for code nobody touched. That failure has a precedent here: see the +# pinned-ruff comment in helper-ci.yml. +# +# Adding a rule is deliberate: add it, fix the fallout in a PR that does only +# that, and it becomes load-bearing from then on. +only_rules: + # Crashes waiting to happen. + - force_try + - force_cast + - implicitly_unwrapped_optional + + # Code that says something other than what it does. + - unused_optional_binding + - unused_closure_parameter + - unused_control_flow_label + - redundant_discardable_let + - redundant_string_enum_value + - redundant_void_return + - unneeded_synthesized_initializer + - unneeded_break_in_switch + + # Readability rules with no plausible false positive. + - control_statement + - for_where + - syntactic_sugar + - empty_count + - empty_string + - is_disjoint + - toggle_bool + - last_where + - first_where + - contains_over_filter_count + - contains_over_first_not_nil + + # Non-negotiable regardless of style. + - inclusive_language + +# force_try defaults to ERROR severity, which is why it — and only it — made +# this job exit 2. Keeping it as an error is correct: an unjustified `try!` in +# this codebase should stop a merge. The justified ones carry a reasoned +# `swiftlint:disable:next` at the site, which is better documentation than +# silence and better safety than a blanket downgrade. + +excluded: + - CLIPulseCore/.build + - build diff --git a/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/ClaudeConversationPreviewFormatter.swift b/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/ClaudeConversationPreviewFormatter.swift index 1d95e2e2..58ad6b5a 100644 --- a/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/ClaudeConversationPreviewFormatter.swift +++ b/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/ClaudeConversationPreviewFormatter.swift @@ -334,6 +334,10 @@ public enum ClaudeConversationPreviewFormatter { /// Only triggers when the punctuation is immediately followed by /// an uppercase ASCII letter (heuristic for sentence boundary). private static let sentencePunctPattern: NSRegularExpression = { + // The pattern is a compile-time string literal, so this cannot fail at + // runtime. Making the property optional would push a nil check onto every + // call site for no safety gain. + // swiftlint:disable:next force_try try! NSRegularExpression(pattern: "([\\.!?])([A-Z])", options: []) }() @@ -399,6 +403,10 @@ public enum ClaudeConversationPreviewFormatter { /// consume `[1a` and leave a dangling `]`. private static let orphanCsiPattern: NSRegularExpression = { let pattern = "\\[[0-9;:?<>=]+[ -/]*[a-zA-Z](?!\\])" + // The pattern is a compile-time string literal, so this cannot fail at + // runtime. Making the property optional would push a nil check onto every + // call site for no safety gain. + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: []) }() diff --git a/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/CodexConversationPreviewFormatter.swift b/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/CodexConversationPreviewFormatter.swift index 2bc09004..2abe49a0 100644 --- a/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/CodexConversationPreviewFormatter.swift +++ b/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/CodexConversationPreviewFormatter.swift @@ -192,6 +192,10 @@ public enum CodexConversationPreviewFormatter { /// emits them. private static let numberedMenuPattern: NSRegularExpression = { let pattern = "^›?\\s*(?:\\(\\d+\\)|\\[\\d+\\]|\\d+\\.)\\s+\\S" + // The pattern is a compile-time string literal, so this cannot fail at + // runtime. Making the property optional would push a nil check onto every + // call site for no safety gain. + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: []) }() @@ -278,6 +282,10 @@ public enum CodexConversationPreviewFormatter { // covers every real-world CSI final byte, and the lookahead // catches the `[1a]`-style footnote false-positive class. let pattern = "\\[[0-9;:?<>=]+[ -/]*[a-zA-Z](?!\\])" + // The pattern is a compile-time string literal, so this cannot fail at + // runtime. Making the property optional would push a nil check onto every + // call site for no safety gain. + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: []) }() diff --git a/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/GeminiConversationPreviewFormatter.swift b/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/GeminiConversationPreviewFormatter.swift index 55ea680c..06e8a89f 100644 --- a/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/GeminiConversationPreviewFormatter.swift +++ b/CLI Pulse Bar/CLIPulseCore/Sources/CLIPulseCore/GeminiConversationPreviewFormatter.swift @@ -223,6 +223,10 @@ public enum GeminiConversationPreviewFormatter { /// optional leading whitespace. private static let numberedMenuPattern: NSRegularExpression = { let pattern = "^\\s*(?:\\(\\d+\\)|\\[\\d+\\]|\\d+\\.)\\s+\\S" + // The pattern is a compile-time string literal, so this cannot fail at + // runtime. Making the property optional would push a nil check onto every + // call site for no safety gain. + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: []) }() @@ -309,6 +313,10 @@ public enum GeminiConversationPreviewFormatter { // covers every real-world CSI final byte, and the lookahead // catches the `[1a]`-style footnote false-positive class. let pattern = "\\[[0-9;:?<>=]+[ -/]*[a-zA-Z](?!\\])" + // The pattern is a compile-time string literal, so this cannot fail at + // runtime. Making the property optional would push a nil check onto every + // call site for no safety gain. + // swiftlint:disable:next force_try return try! NSRegularExpression(pattern: pattern, options: []) }()