/**
* PURPOSE: Self-documenting code enforcement - named constants over magic numbers
* QUERY: "Bu satırda naked magic number var mı?"
* YIELDS: RuleCheckResult (violation if magic number found)
*/
import type { LineRule, LineRuleCheckParams, RuleCheckResult } from '../../types'
// QUERY: "Hangi sayılar ve pattern'ler hariç tutulur?"
const ALLOWED_NUMBERS = new Set([
0, 1, -1, 2, 10, 100, 1000,
// HTTP status codes
200, 201, 204, 301, 302, 304,
400, 401, 403, 404, 405, 409, 422, 429,
500, 502, 503, 504
])
export const noMagicNumberRule: LineRule = {
name: 'onMagicNumber',
active: true,
exclude: { paths: ['Config', 'Constants', 'Tests', 'rules/'], patterns: [] },
check(params: LineRuleCheckParams): RuleCheckResult {
// Skip constant declarations
if (params.trimmed.match(/^(static\s+)?(let|var)\s+[a-z][A-Za-z]*\s*[=:]/)) {
return { matched: false }
}
// Skip enum cases
if (params.trimmed.match(/case\s+\w+\s*=/)) {
return { matched: false }
}
// Remove string literals to avoid false positives in examples/templates
const withoutStrings = params.trimmed
.replace(/"[^"]*"/g, '""')
.replace(/"""[\s\S]*?"""/g, '""')
// Find standalone numbers (not part of identifiers)
// \d in lookbehind prevents matching substrings like "970" in "1970"
const numberMatches = withoutStrings.match(/(?<![A-Za-z_.\d])\d{2,}(?![A-Za-z_\d])/g)
if (numberMatches) {
for (const match of numberMatches) {
const num = parseInt(match, 10)
if (!ALLOWED_NUMBERS.has(num) && num > 10) {
return {
matched: true,
violation: generateViolation({ line: params.line, code: params.trimmed, number: match })
}
}
}
}
return { matched: false }
}
}
function generateViolation(params: { line: number; code: string; number: string }): string {
return `[swift] OnMagicNumber violation
Line: ${params.line}
Found magic number: ${params.number}
Code: ${params.code}
CONSTRAINT: Use named constants instead of magic numbers (or use *Config, *Constants)
CORRECT:
private let timeoutSeconds = 30
private let maxRetries = 3
static let secondsPerDay = 86400`
}