# SwiftLint configuration generated from rules.yml  -  LOCAL USE ONLY.
#
# By decision this is NOT part of the project: no committed .swiftlint.yml, no build phase, no
# CI job. Run it through ./lint-local.sh, which resolves the placeholders below and keeps the
# resolved config, the baseline and every report outside the repository.
#
#   ./lint-local.sh Domains/<Module> --write-baseline   # grandfather what exists today
#   ./lint-local.sh Domains/<Module> --strict           # then only new violations surface
#
# Because there is no CI gate, the ratchet is a habit rather than a wall: run it before opening
# a PR. That is weaker than a gate and the audit says so  -  adopting it project-wide stays an
# open decision, not a forgotten one.
#
# Every custom rule identifier IS its rules.yml ID, lowercased, so a CI failure points
# straight at the registry entry.
#
# Placeholders in ((double parens)) are resolved per module by the audit:
#   ((SENSITIVE))  - alternation built from the module's sensitive-data inventory (Phase 2d)
#   ((TRANSIENT))  - the subset the inventory marks required-transient
#   ((SIBLINGS))   - the module registry's other feature module names
#   ((LOGIC))      - the module's logic paths (Domain, Data, Presentation view models)

included: []
excluded:
  - .build
  - "**/Package.swift"
  - Tests/**/*Snapshot*
  - DerivedData
  - "**/Generated"
  - "**/*.generated.swift"

analyzer_rules:
  - unused_declaration
  - unused_import

opt_in_rules:
  - file_header
  - force_unwrapping
  - implicitly_unwrapped_optional
  # missing_docs is deliberately NOT enabled: this project's own rule is "no comment unless it
  # earns its place", and a doc-per-public-declaration mandate manufactures exactly the
  # restate-the-signature noise that rule exists to prevent.
  - prefer_self_in_static_references
  - empty_count
  - first_where
  - last_where
  - redundant_nil_coalescing
  - unowned_variable_capture
  - weak_delegate
  - private_outlet
  - overridden_super_call
  - prohibited_super_call

disabled_rules:
  # WARN-02 owns TODO policy (untagged only); the built-in flags every TODO  -  pure noise.
  - todo
  - line_length
  - trailing_comma
  - opening_brace
  - statement_position
  # SwiftUI's own presentation API is `sheet(isPresented:onDismiss:) { content }`  -  a closure
  # argument plus a trailing view builder. The rule fires on Apple's documented shape, so
  # obeying it would make every sheet read worse than the framework intends.
  - multiple_closures_with_trailing_closure

# READ-03
file_length:
  warning: 400
  error: 900
type_body_length:
  warning: 350
  error: 800
function_body_length:
  warning: 40
  error: 100

# READ-07  -  the component names are bound to an external design mapping and cannot be
# shortened, so the ceiling reflects the codebase rather than SwiftLint's 40-char default.
type_name:
  max_length:
    warning: 62
    error: 75

# STRUCT-01  -  a pure constants namespace groups by nesting (`AppConstant.DeepLink.Path`);
# two levels is the depth that grouping needs, and nothing deeper is allowed.
nesting:
  type_level: 2

identifier_name:
  min_length: 2
  excluded: [id, on, up, x, y]
cyclomatic_complexity:
  warning: 10

# READ-09  -  the built-in takes no per-rule `excluded`; Package.swift is excluded globally below
file_header:
  # Author is the FULL git identity, department included  -  "NAME - <Department> Mudurlugu".
  # A bare name is not enough: it does not say which team owns the file.
  required_pattern: '\A//\n//  .*\.swift\n//  .*\n//(?:\n//.*)*?\n//  Created by .+ - .+ on \d{2}\.\d{2}\.\d{4}\.\n//\n'

# VIS-04  -  only the module's public surface must be documented
# An extension is a grouping, not an API  -  its members carry the docs. Requiring a comment on
# the `extension` line itself only produces restatements of the MARK above it. A memberwise
# init is likewise self-describing.
missing_docs:
  warning: [public, open]
  excludes_extensions: true
  excludes_inherited_types: true
  excludes_trivial_init: true

custom_rules:
  # ── SEC ─────────────────────────────────────────────────────────────────
  sec_01_no_plaintext_persistence:
    name: "SEC-01 sensitive value in unprotected storage"
    regex: '(UserDefaults|NSUbiquitousKeyValueStore)[^\n]*\b((SENSITIVE))\b'
    message: "Sensitive data must not live in UserDefaults. Walk persistence_decision: transient -> memory only; survives restart -> Keychain with an explicit accessibility class."
    severity: error

  sec_01_no_over_persistence:
    name: "SEC-01 transient value being persisted"
    regex: '(credentialStore|keychain|UserDefaults|FileManager)[^\n]*\b((TRANSIENT))\b'
    message: "This value is required-transient. Persisting it  -  Keychain included  -  creates a cleanup obligation with no owner. Hold it in the flow's model and drop it."
    severity: error

  sec_01_keychain_sync:
    name: "SEC-01 keychain item allowed to sync"
    regex: 'kSecAttrSynchronizable[^\n]*(kCFBooleanTrue|true)'
    message: "Credential keychain items must not sync to iCloud."
    severity: error

  sec_01_keychain_accessibility_missing:
    name: "SEC-01 keychain write without an accessibility class"
    regex: 'kSecClassGenericPassword(?:(?!kSecAttrAccessible)[\s\S]){0,400}SecItemAdd'
    message: "Declare kSecAttrAccessible explicitly  -  device-only unless there is a documented reason."
    severity: error

  sec_02_hardcoded_secret:
    name: "SEC-02 hardcoded secret"
    regex: '(?i)\b(api[_-]?key|secret|password|passphrase|bearer|private[_-]?key|client[_-]?secret)\b\s*[:=]\s*"[^"]{8,}"'
    message: "Anything committed is already leaked. Inject configuration at build or runtime."
    severity: error

  sec_03_no_print:
    name: "SEC-03 print()"
    regex: '(?<![\w.])print\('
    message: "Use the module's Logger. print() reaches the device log unredacted."
    severity: error

  sec_03_sensitive_in_log:
    name: "SEC-03 sensitive value interpolated into a log"
    regex: '(logger|log|os_log)[^\n]*\\\([^)]*\b((SENSITIVE))\b'
    message: "Never log this class of value. If it must appear, hash or truncate it and mark the interpolation private."
    severity: error

  sec_03_public_interpolation:
    name: "SEC-03 log interpolation forced public"
    regex: 'privacy:\s*\.public'
    message: "Public interpolation is opt-in per value and must be provably non-sensitive. Justify with a standard:exception marker."
    severity: warning

  sec_03_raw_body_log:
    name: "SEC-03 raw request/response body logged"
    regex: '(logger|log|os_log|debugPrint)[^\n]*\\\([^)]*\b(response|request|body|payload|data)\b\s*\)'
    message: "Never log a raw request or response body."
    severity: error

  sec_04_insecure_scheme:
    name: "SEC-04 http:// literal"
    regex: '"http://(?!localhost|127\.0\.0\.1)'
    message: "HTTPS only. An ATS exception needs a written reason and an expiry."
    severity: error
    excluded: ".*/Tests/.*"

  sec_09_commented_out_compilation_condition:
    name: "SEC-09 compilation condition commented out"
    # [ \t]* not \s*  -  \s matches newlines, so "//" ending a file header plus a real
    # "#if" two lines below would match as one commented-out directive.
    regex: '^[ \t]*//[ \t]*#(if|endif|else)\b'
    message: "A commented-out #if DEBUG is not a guard  -  the code inside it ships. Restore the directive or delete the code."
    severity: error

  # ── TEST ────────────────────────────────────────────────────────────────
  test_01_environment_reached_for:
    name: "TEST-01 environment reached for instead of injected"
    regex: '\b(Date\(\)|UUID\(\)|Locale\.current|TimeZone\.current|Calendar\.current|\.random\(in:)'
    message: "Inject time, randomness, identifiers and locale. A type that reaches for them is untestable by construction."
    severity: warning
    included: ((LOGIC))

  test_03_singleton_in_logic:
    name: "TEST-03 singleton access in the logic layer"
    regex: '\b\w+\.shared\b'
    message: "Resolve at the boundary (init / DI), not inline at the call site."
    severity: warning
    included: ((LOGIC))

  test_05_unstructured_task:
    name: "TEST-05 unstructured Task in logic"
    regex: '(?<!await )\bTask\s*\{'
    message: "An unowned task outlives its screen. Give it an owner and a cancellation story, or restructure."
    severity: warning
    included: ((LOGIC))

  test_05_sleep_in_test:
    name: "TEST-05 sleep in a test"
    regex: '\b(Thread\.sleep|usleep)\(|(?<![.\w])sleep\('
    message: "Use an expectation or an injected clock."
    severity: error

  # ── MOD ─────────────────────────────────────────────────────────────────
  mod_01_no_sibling_import:
    name: "MOD-01 feature module importing a sibling feature"
    regex: '^import\s+((SIBLINGS))\b'
    message: "A feature module never imports another feature module. Route it through the seam layer."
    severity: error

  # ── NAME ────────────────────────────────────────────────────────────────
  name_01_output_handler:
    name: "NAME-01 handler named output"
    regex: '\b(var|let)\s+output\s*:|\boutput\s*:\s*\('
    message: "The navigation handler has one name across the module."
    severity: error

  name_02_transport_in_presentation:
    name: "NAME-02 transport type in the presentation layer"
    regex: '\b\w+Dto\b'
    message: "Transport suffixes stop at the data layer; map into our own model."
    severity: error
    included: ".*/Presentation/.*"

  # ── VIS ─────────────────────────────────────────────────────────────────
  vis_01_non_final_class:
    name: "VIS-01 non-final class"
    regex: '^\s*(public\s+|internal\s+|fileprivate\s+|private\s+)?class\s+(?!func)'
    message: "Classes are final unless a real subclass exists in the module."
    severity: warning

  # ── CONC ────────────────────────────────────────────────────────────────
  # Production code only. A test double is constructed inside one test and mutated by that
  # test's single task, so the hatch carries no concurrency risk there  -  flagging every spy
  # would bury the production hits that do matter.
  conc_03_escape_hatch:
    name: "CONC-03 concurrency escape hatch"
    regex: '(@preconcurrency|nonisolated\(unsafe\)|@unchecked\s+Sendable)'
    included: ".*/Sources/.*"
    message: "Migration tool, not a design tool. Add a standard:exception marker with a removal condition  -  these are counted."
    severity: warning

  # SVC-07  -  a repository protocol method names the service it calls. The legitimate
  # exceptions (variants over one service, no endpoint yet) are meant to surface here and be
  # answered with a standard:exception marker, not to be silently allowed by the pattern.
  svc_07_service_named_method:
    name: "SVC-07 repository method not named after its service"
    regex: '^\s*func (?!send)[a-z]\w*\('
    included: ".*RepositoryProtocol\\.swift"
    message: "Name it `send<ServiceName>` (check-open-status -> sendCheckOpenStatus), or mark it standard:exception(SVC-07) with the reason."
    severity: warning

  # SVC-08  -  arithmetic or a policy default inside a mapper. Optional lowering (`?? ""`,
  # `?? false`, `?? []`, `?? nil`) is deliberately not matched; a literal number or a division is.
  svc_08_rule_in_mapper:
    name: "SVC-08 calculation or policy default inside a mapper"
    regex: '(\s/\s*\d|\*\s*\d{2,}|\bmax\(|\bmin\(|\?\?\s*\d{2,}\b)'
    included: ".*Mapper[^/]*\\.swift"
    message: "A mapper moves values. Carry the wire value and do the arithmetic, clamp or default in the view model."
    severity: warning

  # READ-04b  -  a component file under Presentation/Components must carry a preview. Reported
  # per file that declares a View and has no #Preview.
  # READ-04b is NOT a swiftlint rule: a regex matches a LINE, and "this file has no #Preview"
  # is a statement about the whole file. Matching `struct X: View {` under Components/ flags
  # every component, previewed or not  -  25 false positives on first run. The check lives in the
  # standard's structural pass instead (component files vs files containing #Preview).

  # PLAT-01  -  raw SwiftUI presentation in a feature module. The app ships an OS-independent
  # wrapper; the system API drifts with every iOS release (the iOS 26 partial-detent inset).
  plat_01_raw_presentation:
    name: "PLAT-01 raw system presentation instead of the app's sheet"
    regex: '(?<![\w.])\.sheet\('
    included: ".*/Sources/.*"
    excluded_match_kinds: [comment, doccomment]
    message: "Use the app's own sheet wrapper. System .sheet only for OS-owned surfaces (share, Siri, photo picker)  -  mark those standard:exception(PLAT-01)."
    severity: warning

  # PLAT-03  -  an asset URL on someone else's host.
  plat_03_third_party_asset_host:
    name: "PLAT-03 asset URL on a third-party host"
    regex: 'https?://(?!([\w.-]*\.)?(example\.com|cdn\.example\.com))[\w.-]+/[^"]*\.(png|jpg|jpeg|svg|webp|gif)'
    excluded: ".*/Tests/.*"
    message: "Assets come from the app's own CDN, and the URL belongs in the module's constants namespace."
    severity: error

  conc_04_legacy_concurrency:
    name: "CONC-04 GCD layered onto async"
    regex: '\b(DispatchQueue\.(main|global)|DispatchSemaphore|DispatchGroup)\b'
    message: "One concurrency model. Structured concurrency already expresses this."
    severity: warning
    included: ((LOGIC))

  # ── READ / SAFE ─────────────────────────────────────────────────────────
  read_02_ai_attribution:
    name: "READ-02 AI or tool attribution"
    regex: "(?i)(generated by (claude|copilot|ai)|🤖 generated|co-authored-by:.*(claude|anthropic|copilot))"
    message: "No AI or tool attribution in source, commits or PRs."
    severity: error

  read_08_raw_hex_color:
    name: "READ-08 raw hex color"
    regex: 'Color\(hex:\s*"|UIColor\(red:'
    message: "Use the design token namespaces."
    severity: error

  read_08_raw_font:
    name: "READ-08 raw font"
    regex: '\.font\(\s*\.(system|custom)'
    message: "Use the typography tokens."
    severity: error

  safe_02_ad_hoc_dateformatter:
    name: "SAFE-02 ad-hoc DateFormatter on a service boundary"
    regex: 'DateFormatter\((?:(?!timeZone)[\s\S]){0,300}?(request|Request|payload|dto|Dto)'
    message: "A calendar day sent to a service shifts a day in the device timezone. Use the shared calendar-day helper."
    severity: error
    included: ".*/(Mapper|Data|Repository)/.*"

  # ── A11Y ────────────────────────────────────────────────────────────────
  a11y_05_absolute_edge:
    name: "A11Y-05 left/right instead of leading/trailing"
    regex: '\.(leftView|rightView)|alignment:\s*\.(left|right)\b|\.padding\(\.(left|right)'
    message: "Use leading/trailing so RTL mirrors."
    severity: error

  # ── PERF ────────────────────────────────────────────────────────────────
  perf_02_index_as_identity:
    name: "PERF-02 index used as list identity"
    regex: 'ForEach\([^)]*\.indices'
    message: "Use a stable identity; index-as-id breaks diffing and reuse."
    severity: warning

  perf_04_formatter_per_render:
    name: "PERF-04 formatter constructed per render"
    regex: '(DateFormatter|NumberFormatter|RelativeDateTimeFormatter)\(\)'
    message: "Hoist it out of the render path  -  construction is expensive and repeats every update."
    severity: warning
    included: ".*(View|Scene)\\.swift"

  # ── DEPR / WARN ─────────────────────────────────────────────────────────
  depr_02_deprecation_without_replacement:
    name: "DEPR-02 deprecation without a stated replacement"
    regex: '@available\([^)]*deprecated(?![^)]*message:)[^)]*\)'
    message: "A deprecation without a replacement is a complaint, not a migration. Say what to use instead."
    severity: error

  depr_04_stale_availability_gate:
    name: "DEPR-04 availability gate at or below the deployment target"
    regex: '#available\(iOS 1[0-7](\.[0-9]+)?'
    message: "Dead branch: the package already requires this version. Remove the gate."
    severity: warning

  warn_02_untagged_todo:
    name: "WARN-02 TODO without an owner or ticket"
    regex: '//\s*TODO(?!\[)'
    message: "Tag it  -  // TODO[TICKET-123]: ...  -  or it is a note to someone who has left."
    severity: warning

  warn_02_fixme:
    name: "WARN-02 FIXME in source"
    regex: '//\s*(FIXME|HACK|XXX)\b'
    message: "Known-broken belongs in the tracker, not the source."
    severity: warning
