Functions And Variables Declared At File Scope

ID

swift.functions_variables_defined_outside_classes

Severity

critical

Remediation Complexity

medium

Remediation Risk

low

Remediation Effort

medium

Resource

Code Smell

Language

Swift

Tags

code-style, encapsulation

Description

Reports a function or a let / var declared at file scope, outside any type or extension, with module-or-wider visibility.

Rationale

let apiBaseURL = "https://example.com"
var requestCounter = 0

func formatCount(_ count: Int) -> String {
    return "n=\(count)"
}

A file-scope declaration with module visibility puts its name into the namespace of the whole module. Three costs follow. Every file in the module can see the name, so nothing tells a reader — or a refactoring tool — which file owns it. The name competes with every other module-level name, and the collision only surfaces when a second file happens to pick it. And a module-wide mutable var is shared state that any file can write with no coordination, which in a concurrent target is a data race with no lock to point at.

Moving the declaration into the type it serves — as a method, a static member, or an extension member — gives it an owner, a scope, and a name a reader can find from the call site.

Notes

Swift, unlike Java or C#, permits file-scope declarations and in several places requires them, so a literal reading of this check would fire on correct code. The trigger is narrowed to the declarations that really do pollute the module namespace, and every shape Swift gives no alternative for is exempt:

  • Entry-point files are skipped entirely — a file listed in exemptFiles, a file declaring a @main type, and any file carrying top-level executable code (which Swift accepts only in a script or entry-point file).

  • private and fileprivate declarations are not reported. At file scope both mean "visible in this file only", so the name never reaches the module namespace and the defect described above does not exist. This is the single biggest narrowing: marking a file-scope helper private is both the smallest fix and a complete one.

  • Operator implementations are not reported. func + (lhs: Point, rhs: Point) must be a free function or a static member, and the free form is idiomatic.

  • Declarations guarded by #if are not reported. A conditional-compilation block wraps its contents in a block of their own, so a platform shim never reaches this rule — the right outcome, since such a shim usually has no type to belong to.

  • Only functions and value bindings are considered. A top-level typealias, operator or precedencegroup declaration has nowhere else to go in Swift.

Configuration

  • exemptFiles (default [main.swift, Package.swift]) — file names, matched case-insensitively against the file name only, whose file-scope declarations are never reported. Add the entry points of any other target here.

Remediation

Give the declaration an owner. A caseless enum is the usual namespace for constants, because it cannot be instantiated:

enum Api {
    static let baseURL = "https://example.com"

    static func formatCount(_ count: Int) -> String {
        return "n=\(count)"
    }
}

When the helper genuinely belongs to one type, put it there as a method or an extension member:

extension Int {
    var formattedCount: String {
        return "n=\(self)"
    }
}

When it is used only inside its own file, private is the whole fix:

private let apiBaseURL = "https://example.com"

Module-wide mutable state needs more than a move: give it a single owner that controls access to it — an actor, or a type whose only mutating entry points are its own methods.

© 2026 Xygeni. All rights reserved