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@maintype, and any file carrying top-level executable code (which Swift accepts only in a script or entry-point file). -
privateandfileprivatedeclarations 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 helperprivateis both the smallest fix and a complete one. -
Operator implementations are not reported.
func + (lhs: Point, rhs: Point)must be a free function or astaticmember, and the free form is idiomatic. -
Declarations guarded by
#ifare 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,operatororprecedencegroupdeclaration 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.