← Back to home

Lint rules

All 83 lint rules Papyrus Lint implements, generated directly from the linter's own rule metadata so this list can never drift out of sync with what actually ships. Search by name, id, or description, or filter by severity, tag, and auto-fix support; each rule's full documented behavior is one click away.

RuleSeverityTagsDescriptionAuto-fix
Argument naming consistency
argument-naming
warningcorrectnessmaintainabilityFlag a function declared on this script whose parameter name doesn't match (case-insensitively) the corresponding parameter of the same-named function declared on the script it Extends.
Full behavior

Flags, as a [warning], a function declared on this script whose parameter name doesn't match (case-insensitively) the corresponding parameter of the same-named function declared on the script it Extends (directly or transitively) — since Papyrus resolves a named-argument call against the declared type of the reference it's called through, a renamed parameter on an override can silently misdirect (or fail to compile) a caller using the parent's names. Only checked when linting a .psc file dropped in the app, by resolving the Extends chain from the project root; a function declared inside a State block is not checked, and only parameter positions present on both declarations are compared.

Argument override type check
argument-override-types
errorcorrectnessmaintainabilityFlags a function or event whose parameter count or types don't match the same-named function declared on the script it Extends.
Full behavior

Flags, as an [error], a function or event declared on this script whose parameter count or parameter types don't match the corresponding parameters of the same-named function declared on the script it Extends (directly or transitively) — a call resolved against a parent-typed reference still binds against the parent's exact declared parameter list, so a mismatched override either fails to compile against such a reference or silently receives arguments meant for a differently-shaped signature. A parameter count mismatch is reported once for the whole declaration; a matching count is then compared type by type (exact match, case-insensitively, with no widening/subtype leniency). Only checked when linting a .psc file dropped in the app, by resolving the Extends chain from the project root; a function declared inside a State block is not checked.

Argument type check
argument-types
errorcorrectnessFlags call-site arguments whose type doesn't match the callee's declared parameter type.
Full behavior

Flags call-site arguments whose type doesn't match the callee's declared parameter type (e.g. passing a String where an Int is expected), allowing the implicit Int-to-Float widening Papyrus itself allows, as well as passing an object whose script extends (directly or transitively) the parameter's type (e.g. passing an Armor where a Form is expected, or an Actor where an ObjectReference is expected). Calls to functions declared in the same script are always checked; when linting a .psc file dropped in the app, calls to functions declared on other scripts under the project root (e.g. SomeProperty.DoThing(...)) are checked too, by resolving those scripts' signatures (including through Extends), and the Extends chain of an argument's own script is likewise resolved from the project root to allow compatible subtypes. Native engine types (e.g. Actor, ObjectReference, Form, Spell) are resolved from the configured script roots like other external scripts, so their declared subtype relationships are recognized. A call whose target or argument type can't be determined is skipped rather than guessed at.

Array bounds
array-bounds
warningcorrectnessFlags a literal index into a local array that falls outside the constant size it was declared with (e.g. new Float[3] followed by a[5]), since Papyrus silently no-ops the access instead of raising an error.
Full behavior

Flags, as a [warning], a literal index into a local array variable that falls outside the compile-time-constant size it was declared with (e.g. Float[] a = new Float[3] followed by a[5] = 0.1), since Papyrus doesn't raise a catchable error for an out-of-range array access — it just logs the mistake and silently no-ops the write or returns the type's default for a read. A variable starts tracking a size the moment it's assigned new <Type>[<N>] for a literal (or literal-arithmetic) N, and stops being tracked the moment it's assigned anything else; a size is only kept past an If when every surviving branch agrees on it, and a size learned only inside a While loop's body is never assumed to still hold afterward, since the loop may run zero times. Only a plain identifier's own index is checked; a member/property array, or an index built from anything other than a literal (optionally combined with arithmetic, comparison, logical, and unary operators), is left unflagged rather than guessed at. A new <Type>[<N>] whose own literal N falls outside the range Papyrus allows is flagged separately, by "Array size range" below.

Array size range
array-size-range
errorcorrectnessFlags, as an error, a new array whose own literal size falls outside 0 to 128, since Papyrus hard-caps a script-created array at 128 elements and a negative size makes no sense; doesn't apply to array Properties or natively-returned arrays, so those aren't checked against it.
Full behavior

Flags, as an [error], a new <Type>[<N>] array creation whose literal N falls outside the range Papyrus allows for a script-created array (0 to 128), since a size outside that range is almost never intended: Papyrus hard-caps an array created with New (or grown with Add()) at 128 elements, and a negative size makes no sense at all. That cap doesn't apply to an array returned by a native function or to an editor-populated array Property, since neither is created this way. Unlike "Array bounds" above, this only ever looks at a new expression's own literal size, with no tracking of which variable holds it.

Spacing around assignment operators
assignment-operator-spacing
warningstyleRequires exactly one space on either side of =, +=, -=, *=, /=, and %=.
Full behavior

Requires, as a [warning], exactly one space on either side of =, +=, -=, *=, /=, and %=. A side whose whitespace reaches a newline (the operator opens or closes a statement continued across physical lines) is left unchecked on that side. The fix normalizes each flagged side to a single space, without reaching across a newline.

Whitespace interrupting property/method chaining
chain-whitespace
warningstyleFlags a space or tab immediately before or after a member/method access.
Full behavior

Flags, as a [warning], a space or tab immediately before or after a . member/method access (e.g. SomeProperty . DoThing()), since it interrupts the chain for no benefit. A . inside a Float literal (e.g. 1.5) is never flagged. The fix closes the gap on whichever side(s) have it, without reaching across a newline (a chain continued onto another physical line is left alone).

Circular script dependency
circular-dependency
warningmaintainabilityFlags a Property whose declared type, followed through that script's own Property declarations, eventually leads back to the script being linted (e.g. script A declaring a B Property while script B declares an A Property). A property typed as its own script isn't flagged. Disabled by default, since two scripts intentionally referencing each other for two-way communication is a common, legitimate design.
Full behavior

Flags, as a [warning], a Property declaration whose declared type, followed through that other script's own Property declarations across the project, eventually leads back to the script being linted (e.g. script A declaring a B Property, while script B declares an A Property), since a cycle like that makes the scripts involved hard to reason about or reuse independently — neither can be fully understood without the other. A property whose own declared type is the script it's declared on (e.g. a linked-list node holding a Property of its own type) is never flagged, since that's a script depending on itself rather than a cycle between scripts. Disabled by default, since two scripts intentionally holding Property references to each other for two-way communication (e.g. a manager and a worker script) is a common, legitimate design, not a mistake; opt in with rules.circular_dependency. Only checked when linting with project context, by resolving other scripts' own declared property types (not extended through Extends) the same way the argument/return type checks resolve their own.

Space after comma
comma-spacing
warningstyleRequires whitespace after commas in argument lists.
Full behavior

Requires, as a [warning], whitespace after commas in argument lists.

Conflicting script versions
conflicting-script-versions
warningcorrectnessFlags same-named scripts with different contents in separate source directories.
Full behavior

Flags, as a [warning], a .psc file when another script search directory contains a case-insensitively same-named file with different contents (determined by MD5), since which version Papyrus resolves can depend on search-directory order. Byte-identical copies are ignored. Only available when linting a file with project context in the desktop app or CLI.

Cyclomatic complexity
cyclomatic-complexity
warningmaintainabilityFlags functions/events whose cyclomatic complexity exceeds a configurable threshold. A configured error threshold below the warning one is treated as equal to it.
Full behavior

Flags functions/events whose cyclomatic complexity (1 plus each If/ElseIf branch, While loop, and short-circuiting &&/|| operator) exceeds a configurable threshold, as a [warning] above cyclomatic_complexity_warning (default 10) or an [error] above cyclomatic_complexity_error (default 20); cyclomatic_complexity_error configured below cyclomatic_complexity_warning is treated as equal to it, since a lower error threshold would otherwise contradict the warning one it's supposed to escalate.

Side-effecting call inside Debug.*
debug-side-effects
warningcorrectnessFlags a call to a side-effecting function nested inside any Debug.* argument list, since that call only runs when the debug output itself runs.
Full behavior

Flags, as a [warning], a function call nested inside any Debug.* argument list (Trace, Notification, MessageBox, TraceStack, …) when that nested call is side-effecting. A same-script function is side-effecting when it writes a property or field, or transitively calls another same-script function that does. A call that can't be proven that way (a native, or a function on another script) is classified by name: prefixes such as Set, Remove, Add, Wait, Enable, Disable, Place, Kill, and similar mutating natives are flagged; Get/Is/Has style getters are left alone. The finding points at the nested call, not the Debug.* call itself.

Default property value
default-property-value
warningstylemaintainabilityFlags a Bool/Int/Float/String Auto/AutoReadOnly property with no explicit default value, since it then silently falls back to Papyrus's own implicit per-type default instead of a value the author chose. Disabled by default.
Full behavior

Flags, as a [warning], a Bool/Int/Float/String Auto/AutoReadOnly property declared with no explicit default value (e.g. Int Property Count Auto rather than Int Property Count = 0 Auto), since it then silently falls back to Papyrus's own implicit per-type default (False, 0, 0.0, or "") instead of a value the author actually chose. Object-typed properties, array-typed properties, and full (non-Auto/AutoReadOnly) properties are never flagged. Disabled by default, since many existing scripts already rely on Papyrus's implicit defaults for some or all of their properties; opt in with rules.default_property_value.

Division by zero
division-by-zero
warningcorrectnessFlags divisions by zero where they are likely to happen.
Full behavior

Flags, as a [warning], a / or % whose right-hand operand is a compile-time-constant zero (e.g. x / 0, x % 0.0, x / (1 - 1), x / (0 / 1)), since that crashes the script at runtime. Only a divisor built entirely from literals (combined with arithmetic, comparison, logical, and unary operators) is checked; one that depends on an identifier, a call, Self/Parent, a member/index access, a cast, or a new array is left unflagged rather than guessed at.

Empty loop/conditional body
empty-body
warningcorrectnessmaintainabilityFlags seemingly useless loops and conditional bodies, since they are likely a mistake.
Full behavior

Flags, as a [warning], since this is almost always a forgotten piece of logic rather than something intentional: a While loop whose body is empty, or whose body only nudges a variable by a constant amount (i += 1, i -= 1, or the equivalent i = i + 1/i = i - 1) with nothing else giving the loop a purpose (a step built from anything but a literal, such as a call or another variable, is left alone since it has a side effect of its own); and an empty If, ElseIf, or Else body. An Else clause is told apart from no Else clause at all (both parse to an empty body) by scanning for a literal Else immediately followed by EndIf in the source.

Event signature mismatch
event-signature-mismatch
warningcorrectnessFlags, as a warning, an Event declaration whose name matches one of the engine's own native events but whose parameter count/types don't match the signature it's actually called with, since a mismatched declaration still compiles but the engine then never invokes it correctly. Disabled by default, since the known-event list is a curated subset and matches by name alone.
Full behavior

Flags, as a [warning], an Event declaration whose name matches one of the engine's own native events, listed in shared/rules/data/known-events.yaml alongside the Form that first declares each one and its exact parameter list, but whose declared parameter count/types don't match that signature, since Papyrus never validates an Event's signature against what the engine actually calls it with — a mismatched declaration still compiles fine, but the engine then never invokes it (or invokes it with arguments the script doesn't expect), so the script silently never receives that event. Matches by event name alone (case-insensitively), regardless of which Form the enclosing script actually Extends, the same way "Non-base-game native function usage" matches by (script, function) name alone; parameter types are compared exactly (case-insensitively, with no widening/subtype leniency), parameter names are not. Disabled by default, since shared/rules/data/known-events.yaml only lists a curated subset of the engine's native events, and a script that declares an Event sharing one of those names without actually extending the listed Form would otherwise be misreported; opt in with rules.event_signature_mismatch.

Exclamation mark spacing
exclamation-spacing
warningstyleFlags a negation operator not followed by exactly one space. In a chained negation such as !!bReady, only the final ! requires a trailing space.
Full behavior

Flags, as a [warning], a ! negation operator not followed by exactly one space (e.g. !bReady or ! bReady), since a bit of breathing room makes the negation easier to spot. Never flags !=, which the lexer tokenizes separately, nor the gap between two directly adjacent !s in a chained/double negation (e.g. !!bReady) — only the last ! in such a run needs its own trailing space. The fix inserts a space where there is none and collapses a longer run of spaces/tabs down to one.

Explicit return on every path
explicit-return
errorcorrectnessFlags a typed function/event with a code path that falls off the end of its body without a Return.
Full behavior

Flags, as an [error], a typed function/event with a code path that falls off the end of its body without a Return, since Papyrus then silently returns that type's default value (0, "", False, or None) instead of one the author chose. A Return with no value still counts as long as it's reached (return_types covers a value's actual type); an If only counts when every branch, including an Else, returns, and a While loop is never assumed to guarantee one since it may run zero times. A native function has no body to inspect and is never flagged.

Float equality comparison
float-equality
infocorrectnessFlags a direct == or != between two floats, since rounding error can make them compare unequal even when conceptually the same. Disabled by default.
Full behavior

Flags, as an [info], a direct ==/!= comparison between two Float values, since floating-point rounding error can make two values that are conceptually the same compare unequal (or vice versa) at runtime. Only comparisons whose operand types can be determined locally are checked, the same restriction "Strict numeric type check" places on its own. Disabled by default, since a project may deliberately compare two Float values it knows are computed the exact same way; opt in with rules.float_equality.

Implicit Float-to-Int conversion
float-to-int
warningcorrectnessFlags a Float value declared, assigned, returned, or passed as an argument into an Int-typed slot without an explicit as Int cast.
Full behavior

Flags a Float value declared, assigned, returned, or passed as an argument into an Int-typed slot without an explicit as Int cast.

Forbidden/discouraged function usage
forbidden-functions
warningperformancecorrectnessFlags calls to functions that are usually unintentional performance bottlenecks. Debug.* already behind a simple debug-flag If (e.g. If IsDebugMode) is left unflagged.
Full behavior

Flags calls to functions listed in shared/rules/data/forbidden-functions.yaml (e.g. slow or blocking native calls), with a configurable severity and an explanatory message per entry. A Debug.* call (Trace, TraceStack, Notification) nested inside an If/ElseIf whose condition is a simple identifier (or Self/Parent/identifier member chain) whose name contains debug (case-insensitively, e.g. If IsDebugMode) is left unflagged, since that is already a debug-flag guard; an Else of that chain, a negated or compound condition, or a name that doesn't look like a debug flag is still flagged.

FormID hex notation
formid-hex-notation
warningcorrectnessstyleFlags a FormID literal compared against GetFormID() or passed to Game.GetFormFromFile that isn't written in hexadecimal.
Full behavior

Flags, as a [warning], a FormID literal that isn't written in hexadecimal notation when it's directly compared (==, !=, <, <=, >, >=) against a GetFormID() call, or passed as the FormID argument to Game.GetFormFromFile (positionally or by name), since hexadecimal is the convention used everywhere else a FormID appears (the Creation Kit, xEdit, mod documentation) and a stray decimal literal is easy to mistype or overlook. Only a literal directly adjacent to the comparison operator or the call's argument list is checked; one reached indirectly through a variable assigned earlier is left unflagged rather than guessed at. Automatically fixable: the fix rewrites the flagged literal's digits into their hexadecimal equivalent (e.g. 76935 becomes 0x12C87) in place.

Inherited function override
function-override
infomaintainabilityInforms about a function declared on this script that shares its name with a function declared on the script it.
Full behavior

Flags, as an [info], a function declared on this script that shares its name with a function declared on the script it Extends (directly or transitively) — the local declaration silently replaces the inherited one. This is often intentional (e.g. overriding an Event OnInit() handler), so it's informational rather than a warning. Only checked when linting a .psc file dropped in the app, by resolving the Extends chain from the project root; a function declared inside a State block is not checked (state-based override is a separate mechanism from Extends).

Game.GetFormFromFile("Skyrim.esm") simplification
get-form-from-file-skyrim-esm
warningperformancestyleFlags Game.GetFormFromFile(id, "Skyrim.esm"), which is exactly what Game.GetForm(id) already does.
Full behavior

Flags, as a [warning], a qualified Game.GetFormFromFile call whose file name argument is the literal "Skyrim.esm" (positionally or by name, matched case-insensitively), since Skyrim.esm is always loaded as master index 0 and a FormID valid for it never needs the file name argument's extra resolution step at all — Game.GetForm(id) returns exactly the same Form for less overhead. Automatically fixable: the fix rewrites the call to Game.GetForm(id), keeping the original FormID argument (including any expression it's part of) and dropping the file name argument and its name if it was passed by name.

GetState() comparison
get-state-comparison
errorcorrectnessFlags a GetState() == "Name"/!= "Name" comparison against an undefined state, since it then always evaluates to the same result instead of raising an error.
Full behavior

Flags, as an [error], a GetState() == "Name"/GetState() != "Name" comparison (bare or self.GetState()) whose named state isn't declared as a State on this script, since a typo'd or renamed state name still compiles fine but the comparison then silently, permanently evaluates the opposite of what was intended — a check against a state that can never be the current one is always false, and its negation always true. GetState() == "", checking whether the script is currently in the empty state, is always valid. Only a literal string operand is checked; one built from anything else is left unflagged rather than guessed at. A target undeclared on this script is only flagged when this script has no Extends target at all, since it may otherwise be declared on a script further up that (unresolved) Extends chain — the same forward-declaration case goto-state allows. When linting a .psc file dropped in the app, that chain is resolved from the project root too, the same way goto-state resolves its own.

GlobalVariable increment via SetValue(GetValue() + x)
global-variable-increment
infoperformanceFlags gv.SetValue(gv.GetValue() + x), since GlobalVariable's native Mod(x) call does the same thing in one call instead of two; the fix rewrites the call to gv.Mod(x).
Full behavior

Flags, as an [info], a SetValue call on a GlobalVariable-like receiver whose sole argument adds something to that very same receiver's own GetValue() (in either operand order, e.g. gv.SetValue(gv.GetValue() + x) or gv.SetValue(x + gv.GetValue())), since GlobalVariable exposes that exact operation as a single native call: Mod(afValue) adds afValue to the global's current value (and returns the new value) in one call instead of two native calls plus an addition. Like "Slow function usage", a call's receiver can't generally be resolved back to a GlobalVariable-typed script, so this matches structurally instead: only a SetValue call standing alone as its own statement, taking exactly one argument that's a top-level + between the same receiver's GetValue() and some other expression, is considered; anything less direct (a different operator, SetValueInt, a receiver that isn't a simple identifier/Self/member chain, ...) is left unflagged rather than guessed at. The fix replaces the whole SetValue(...) call with Mod(x), preserving x's own source text and leaving the receiver qualifier untouched.

GlobalVariable no-op write
global-variable-setvalue
warningcorrectnessmaintainabilityFlags a SetValue/SetValueInt call that doesn't provably change the value: a branch writing back the exact value its own GetValue() == literal condition just confirmed, or an Else branch writing a literal with no check ruling out that value already being current. Disabled by default.
Full behavior

Flags, as a [warning], a SetValue/SetValueInt call on a GlobalVariable-like receiver that writes a value an enclosing If/ElseIf/Else chain never proves is different from the value already there — either a branch writing back the exact literal its own GetValue()/GetValueInt() == literal condition just confirmed is already current, or the trailing Else of a chain that reads the same receiver elsewhere writing a literal with no condition of its own ruling out that value already being current, e.g. an Else unconditionally calling gv.SetValue(0.0) after an If gv.GetValue() == 1.0 branch, where it should usually become an explicit ElseIf gv.GetValue() != 0.0 instead. Only a SetValue/SetValueInt call standing alone as its own statement, guarded by a plain equality check against a literal, is considered; anything less direct is left unflagged rather than guessed at. Disabled by default, since the Else case is a heuristic rather than a proven no-op; opt in with rules.global_variable_setvalue.

GoToState state reference
goto-state
warningcorrectnessFlags states that are undefined, likely because of typos.
Full behavior

Flags, as a [warning], a GoToState("Name") call (bare or self.GoToState(...)) whose target isn't declared as a State on this script, since a typo'd or renamed state name still compiles — the engine just silently falls back through its state resolution algorithm instead of raising an error, so the call quietly never takes effect. GoToState(""), which switches back to the empty state, is always valid. Only a literal string argument is checked; one built from anything else is left unflagged rather than guessed at. A target undeclared on this script is only flagged when this script has no Extends target at all, since it may otherwise be declared on a script further up that (unresolved) Extends chain — a legitimate way to forward-declare a state for a not-yet-written child script to implement. When linting a .psc file dropped in the app, that chain is resolved from the project root too, the same way the argument/return type checks resolve their own, so a target declared on an ancestor script is recognized rather than flagged.

Identifier casing
identifier-casing
warningstyleFlags a declared function/event, property, state, parameter, or local/script variable whose name doesn't match the configured style.
Full behavior

Flags a declared function/event, property, state, parameter, or local/script variable whose name doesn't match the configured identifier_casing style: camelCase, PascalCase, snake_case, or CONSTANT_CASE. ScriptName itself is never checked by this lint (see "Type name casing" below). A parameter has no location of its own, so it's reported on its enclosing function's line. The automatic fix renames each flagged declaration and its references only when the conversion preserves every underscore in its original position; fixes that would add, remove, or move underscores are left for the user because they constitute a substantive rename.

Impossible cast
impossible-cast
warningcorrectnessFlags an As cast proven to never succeed, e.g. Weapon b = a as Weapon where a is an Armor, since Armor and Weapon are unrelated types. Only flagged once both types are confirmed to resolve to a known root.
Full behavior

Flags, as a [warning], an explicit as cast proven to never succeed: neither the value's known type nor the cast's target type extends the other, directly or transitively (e.g. Armor a followed by Weapon b = a as Weapon, since Armor and Weapon are unrelated types that both directly extend Form), so the cast always evaluates to None no matter what the value actually holds. Only a cast whose value's type can be determined locally (locals, parameters, properties, Self/Parent, literals, and other resolvable expressions) is checked, the same restriction "Useless downcast" places on its own value type; primitive types (Int, Float, Bool, String) are never flagged. Since Papyrus scripts have single inheritance, two types are unrelated exactly when neither's Extends chain reaches the other, but this is only ever flagged once both the value's and the target's chains are confirmed to resolve all the way to a definite root — a resolved script with no Extends at all — rather than merely failing to find a relation for lack of data; only checked when linting a .psc file dropped in the app, the same way "Useless downcast" resolves an ancestor-type cast.

Formatting checks
indentation
warningstyleFlags lines whose indentation doesn't match the configured style/width.
Full behavior

Flags, as a [warning], lines whose indentation doesn't match the configured style/width (indentation/indentation_width) for their nesting depth. A script whose structure can't be identified (e.g. it doesn't lex cleanly) is left unchecked rather than guessed at.

Int/Int division widened to Float
int-division-to-float
warningcorrectnessFlags an Int/Int division declared, assigned, returned, or passed as an argument into a Float-typed slot, since Papyrus truncates the division before it ever widens into the Float (e.g. Float f = 1 / 2 yields 0.0, not 0.5). A constant division that divides evenly (e.g. 72 / 8) isn't flagged.
Full behavior

Flags an Int / Int division declared, assigned, returned, or passed as an argument into a Float-typed slot without either operand already being a Float, since Papyrus performs the division as integer division — truncating towards zero — before the result ever widens into the Float slot (e.g. Float f = 1 / 2 yields 0.0, not 0.5). Casting the division's *result* to Float doesn't avoid this, only casting (or writing) an *operand* as a Float does, so only the latter is left unflagged. When both operands are integer literals (optionally combined with +/-/*/unary -) and the division happens to divide evenly (e.g. Float f = 72 / 8), no truncation actually occurs, so that case is left unflagged too.

Invalid random range
invalid-random-range
errorcorrectnessFlags, as an error, a Utility.RandomInt/RandomFloat call whose first argument isn't smaller than its second, since that never produces any actual randomness.
Full behavior

Flags, as an [error], a call to Utility.RandomInt or Utility.RandomFloat whose first two arguments both fold to compile-time-constant numbers with the first not smaller than the second, since both native functions require their first argument (the minimum) to be smaller than their second (the maximum) — a call with the bounds equal or reversed never produces any actual randomness. Utility.RandomInt/Utility.RandomFloat are only matched when qualified by that literal script name, the same way the "Short wait/update interval" lint treats Utility.Wait. Only an argument built entirely from literals (combined with arithmetic, comparison, logical, and unary operators) is checked; one that depends on an identifier, a call, Self/Parent, a member/index access, a cast, or a new array is left unflagged rather than guessed at.

Invariant loop condition
invariant-loop-condition
warningcorrectnessFlags a While loop whose condition depends on a local variable/parameter that's never assigned anywhere in its own body, since it can then never stop or never run.
Full behavior

Flags, as a [warning], a While loop whose condition depends on a local variable or parameter that's never assigned (plainly or via a compound +=/-=/etc.) anywhere in the loop's own body, since a local variable/parameter can only ever change through a direct assignment inside the function that owns it — nothing else can reach in and modify it — so the condition can then never change once the loop starts: it either never runs at all, or never stops. Only a condition built entirely from identifiers, literals, and arithmetic/comparison/logical/unary operators is checked, the same restriction "Static condition" and "Division by zero" place on their own expressions; one reaching a call, a member/index access, Self/Parent, a cast, or a new array is left unflagged rather than guessed at, since its value may depend on state this lint can't see change (e.g. a.IsDead(), which can start returning something different purely because of what happens inside that call). An identifier that isn't a known local variable or parameter of the enclosing function — most notably a script Property, which another function or the engine is free to change at any time — disqualifies the whole condition rather than being assumed safe. This deliberately doesn't catch a loop that reassigns its exit condition to the very same value on every iteration (e.g. re-fetching a reference that just happens to keep coming back dead), since only running the script could prove that.

Local variable shadowing
local-variable-shadowing
warningcorrectnessmaintainabilityFlags a local variable whose name matches (case-insensitively) a Property or a plain script-level variable declared on the same script.
Full behavior

Flags a local variable (declared with Type name = ... inside a function/event) whose name matches (case-insensitively) a Property or a plain script-level variable (a field, declared without the Property keyword) declared on the same script, since referencing that name inside the function then reads the local rather than the property/field. When linting a .psc file dropped in the app, a local that instead shadows a property or field declared on a parent script (resolved through Extends) is flagged too.

Magic numbers
magic-numbers
warningmaintainabilityFlags numeric literals used directly instead of through a named constant, property, or local variable. -1, 0 and 1 are never flagged. Loose mode (default) exempts Wait/RegisterFor(Single)Update(GameTime) intervals, Strict checks them too. Disabled by default.
Full behavior

Flags, as a [warning], a numeric literal used directly in an expression rather than through a named constant, property, or local variable. -1, 0, and 1 are never flagged, since they're near-universally used directly without losing any clarity. A literal that's the entire value given to a declaration or assignment (Int kMaxTargets = 5, later reassigned as kMaxTargets = 6) is left alone too, since naming it there already gives it the meaning this lint is after; a literal nested inside a more complex initializer (Int kMaxTargets = 5 + 1) is still checked. Disabled by default; opt in with rules.magic_numbers. The configurable magic_numbers setting controls how a Utility.Wait/RegisterForUpdate/RegisterForSingleUpdate/RegisterForUpdateGameTime/RegisterForSingleUpdateGameTime call's interval argument is treated: loose (the default) leaves it unflagged, since a hardcoded interval there is common and usually self-explanatory; strict checks it like any other argument.

Missing documentation comment
missing-doc-comment
warningmaintainabilitystyleFlags a ScriptName, Property, or Function/Event declaration with no {...} documentation comment on the line right after it. Disabled by default.
Full behavior

Flags, as a [warning], a script header (ScriptName), Property declaration, or Function/Event declaration with no documentation comment (CreationKit's own { ... } syntax, rendered as a tooltip in the script picker or property editor) on the line immediately following it. Disabled by default, since most existing scripts have no documentation comments at all and enabling it would otherwise flag literally every declaration in such a project at once; opt in with rules.missing_doc_comment.

Missing update event handler
missing-update-handler
warningcorrectnessFlags, as a warning, a RegisterForUpdate/RegisterForSingleUpdate/RegisterForUpdateGameTime/RegisterForSingleUpdateGameTime call with no matching OnUpdate/OnUpdateGameTime Event declared anywhere in the script, since the registration then has no effect. Disabled by default, since a matching Event declared on a script it Extends would otherwise be misreported as missing.
Full behavior

Flags, as a [warning], a call to RegisterForUpdate, RegisterForSingleUpdate, RegisterForUpdateGameTime, or RegisterForSingleUpdateGameTime in a script that declares no matching Event (OnUpdate or OnUpdateGameTime, per shared/rules/data/update-event-handlers.yaml) anywhere in it, since the engine then has nothing to call once the registered timer fires and the registration has no effect. Matches by function name alone (case-insensitively), regardless of receiver, the same way "Forbidden/discouraged function usage" does; a matching Event is recognized in any State block, not just the empty state. Disabled by default, since it only ever sees a single script's own source, and a matching Event declared on a script it Extends would otherwise be misreported as missing; opt in with rules.missing_update_handler.

Multiple Auto states
multiple-auto-states
errorcorrectnessReports multiple Auto states in one script as an error, or multiple across its ancestry as a warning.
Full behavior

Flags more than one Auto state declared in a single script as an [error], since a script may only declare one. It also flags, as a [warning], multiple Auto states found only after combining a script with every State declared anywhere in its Extends ancestry (as above). The engine tolerates a parent and child each declaring an Auto state (the child's takes precedence at startup), but relying on that precedence is fragile: removing the child's Auto state silently switches its startup state back to the parent's. Only the script's own declared states are considered when linting in isolation; when linting a .psc file dropped in the app, its Extends ancestry is resolved from the project root too.

Prefer named arguments
named-arguments
warningstylemaintainabilityFlags a positional call argument that the configured setting prefers to see passed by Papyrus's named-argument syntax instead; the fix inserts the matching parameter name ahead of it.
Full behavior

Flags, as a [warning], a positional call argument that the configured named_arguments setting prefers to see passed by Papyrus's named-argument syntax instead (func(argB = 1)): always flags every positional argument, instead_of_defaults flags only an argument filling a parameter that has a default value, and never (the default) flags nothing. Parameter names and default values are only known for functions declared in the script being linted (including via self.Func(...)), so a call to a function declared on another script is never flagged. An argument already passed by name is always accepted regardless of setting.

Non-base-game native function usage
native-function-usage
warningmaintainabilityFlags a Native function/event whose name isn't one of the base game's own, a sign it needs SKSE/F4SE or another native extension. Disabled by default.
Full behavior

Flags, as a [warning], a Native function/event declared on a script whose name isn't one of the base game's own native functions, listed in shared/rules/data/native-methods.yaml — a strong signal it's instead supplied by SKSE/F4SE or some other native extension the project depends on. Disabled by default, since plenty of mods intentionally depend on such an extension and don't need to be warned about it; opt in with rules.native_function_usage.

Non-static function call
non-global-function-call
errorcorrectnessFlags a ScriptName.Function() call whose target function resolves but isn't declared Global, since Papyrus's static call syntax can only reach Global functions.
Full behavior

Flags, as an [error], a call through Papyrus's static/global call syntax (e.g. MyScript.DoThing()) whose target function resolves but isn't declared Global on that script, since Papyrus only allows that syntax to reach a script's Global functions — calling an ordinary instance function that way fails to compile. Uses the same "bare identifier not already known as a local variable, parameter, or property" rule as the "Unresolved script reference" lint above to tell a script reference apart from an instance call; a call whose script or function can't be resolved at all is left unflagged (see that lint instead). Only checked when linting with project context, by resolving the target script's functions the same way the argument/return type checks do.

None used as an existing Form
none-form-usage
warningcorrectnessFlags a member/method access on a local variable or script-level property that's still known to be None. Set assume_auto_properties_filled to stop assuming an unset Auto/AutoReadOnly property starts out None; it's still tracked once code assigns it None directly.
Full behavior

Flags a member/method access (a.GetName(), a.Name) on a local variable or script-level Auto/AutoReadOnly property that's still known to be None (e.g. Armor a = None followed directly by a.GetName()), since that crashes the script at runtime. An object-typed local without an initializer, or an object-typed Auto/AutoReadOnly property with no explicit default value (or an explicit = None), starts out None in every function, since it may not be set until something outside the script (the CK's Property Manager, another script, OnInit, …) does so — unless assume_auto_properties_filled is set, in which case a property doesn't start out possibly None on its own; it's still tracked the same as a local variable once script code assigns it None directly. From there, a variable/property is tracked as None from its declaration/assignment until it's reassigned something else, narrowing through If/ElseIf/Else branches guarded by a direct None check (x == None, x != None, !x, a bare x, optionally combined with &&/||) and through a While loop's condition (this language has no break/continue, so the loop can only exit once its condition is false). A branch that unconditionally Returns doesn't carry its state past the If, covering the common If x == None / Return guard idiom. Anything less direct is left unflagged rather than guessed at.

Strict numeric type check
numeric-comparison
warningcorrectnessFlags implicit comparisons between ints and floats that rely on Papyrus doing type conversions.
Full behavior

Flags implicit comparisons (==, !=, <, <=, >, >=) between an Int value and a Float value without an explicit cast making the comparison exact. Only comparisons whose operand types can be determined locally are checked.

Spacing around logical/comparison operators
operator-spacing
warningstyleRequires exactly one space on either side of logical operators.
Full behavior

Requires, as a [warning], exactly one space on either side of &&, ||, ==, !=, >, <, >=, and <=. A side whose whitespace reaches a newline (the operator opens or closes a statement continued across physical lines) is left unchecked on that side. The fix normalizes each flagged side to a single space, without reaching across a newline.

Parameter reassignment
parameter-reassignment
warningmaintainabilityFlags a function/event parameter assigned a new value anywhere in its own body.
Full behavior

Flags, as a [warning], a function/event parameter assigned a new value anywhere in its own body (total = 1, total += 1, ...), since reusing the parameter's name for a different value discards what the caller passed in and can confuse a reader expecting it to still reflect the original argument. A member/index assignment built from a parameter (akRef.Foo = 1), or a reassignment of an unrelated local variable, is never flagged.

Property sorting
property-sorting
warningstyleFlag declaration that isn't sorted by type and then alphabetically by name, or that isn't declared immediately after the ScriptName line, before any variable, function, or state declaration.
Full behavior

Flags, as a [warning], a Property declaration that isn't sorted by type and then alphabetically by name, or that isn't declared immediately after the ScriptName line, before any variable, function, or state declaration (an Import isn't tracked closely enough to count against this). Disabled by default, since reordering a script's declared properties is a more invasive change than the rest of these lints; a project opts in via rules.property_sorting. The fix relocates each property's own declaration lines (its full Property/EndProperty block, for a non-auto property) as a group right after ScriptName, in sorted order; a documentation comment placed directly above a property is left behind rather than moved with it.

Read-only (AutoReadOnly) property write
readonly-property-write
errorcorrectnessFlags an assignment to a property declared AutoReadOnly, since Papyrus rejects that at compile time. Matches the property's bare name or Self.PropertyName; a bare name shadowed by a same-named local/parameter is left alone.
Full behavior

Flags, as an [error], an assignment (=, +=, -=, ...) targeting a script-level property declared AutoReadOnly (e.g. Float Property a = 0.1 AutoReadOnly), since Papyrus rejects that assignment at compile time — an AutoReadOnly property can only ever hold its declared initial value. Matched by the property's bare name or as Self.PropertyName; a bare name shadowed by a same-named local variable or parameter in the enclosing function refers to that local/parameter instead and is never flagged, while a Self.-qualified write is always flagged regardless of shadowing.

Repeated GlobalVariable.GetValue() calls
repeated-getvalue
infoperformanceFlags GetValue() called on the same global more than once across an If/ElseIf chain's conditions, since it can be cached in a local variable instead. Disabled by default.
Full behavior

Flags, as an [info], a GetValue() call repeated on the same receiver across the conditions of a single If/ElseIf chain (e.g. If gv.GetValue() == 1.0 / ElseIf gv.GetValue() == 2.0), since none of the chain's earlier branch bodies run before a later condition is evaluated, so the value can't have changed between those reads — it can be read into a local variable once ahead of the chain instead. Like "Slow function usage", a call's receiver can't generally be resolved back to a GlobalVariable-typed script, so this matches by the GetValue method name alone (case-insensitively, with no arguments); it's the only native method with that name (see shared/rules/data/native-methods.yaml), so this doesn't misfire on unrelated types. Disabled by default, since a chain that reads the same global more than once is often written that way deliberately for readability and the performance cost is usually negligible outside a hot code path; a project opts in via rules.repeated_getvalue.

Repeated Actor.SetOutfit() calls
repeated-setoutfit
warningcorrectnessFlags a second SetOutfit(...) call passing the exact same outfit (and other arguments) as an earlier call on the same receiver, with nothing in between guaranteed to have changed what they're wearing, since a redundant repeated SetOutfit call is known to sometimes leave the actor with no visible equipment. Matches by the SetOutfit method name alone, case-insensitively, without resolving the receiver to an Actor.
Full behavior

Flags, as a [warning], a second SetOutfit(...) call passing the exact same argument(s) as an earlier call on the same receiver, with nothing between them guaranteed to have changed what that receiver is wearing (e.g. akActor.SetOutfit(MyOutfit) followed later by another akActor.SetOutfit(MyOutfit)), since Bethesda's engine is known to mishandle a redundant repeated SetOutfit call, sometimes leaving the actor with no visible equipment until something re-equips them. Like "Repeated GlobalVariable.GetValue() calls", a call's receiver can't generally be resolved back to an Actor/ActorBase-typed script, so this matches by the SetOutfit method name alone (case-insensitively); the whole argument list is compared, so a different abSleepOutfit flag is never flagged as a repeat. Only tracks calls within the same straight-line statement list, resetting fresh (from a copy of the outer state) on entering a nested If/Else/While body, so a change made only inside one never carries forward to code after it.

Return type check
return-types
errorcorrectnessFlags statements whose value's type doesn't match the enclosing function's declared return type.
Full behavior

Flags Return statements whose value's type doesn't match the enclosing function's declared return type (e.g. returning a String from a Function declared Int), allowing the implicit Int-to-Float widening Papyrus itself allows, as well as returning an object whose script extends (directly or transitively) the declared return type (e.g. returning an Armor from a Function declared Form, or an Actor from a Function declared ObjectReference). When linting a .psc file dropped in the app, a returned value's own script's Extends chain is resolved from the project root to allow compatible subtypes there too, including engine types like Actor/ObjectReference/Form resolved from the configured script roots. A Return whose value's type can't be determined, or with no declared return type, is skipped rather than guessed at.

ScriptName/filename mismatch
script-filename-mismatch
errorcorrectnessFlags a .psc file whose declared ScriptName doesn't match its own file name, aside from casing, since Papyrus rejects that at compile time. A namespaced name (e.g. User:MyScript) is compared by its final segment only.
Full behavior

Flags, as an [error], a .psc file whose declared ScriptName doesn't match its own file name, aside from casing (e.g. ScriptName Example in a file named Other.psc), since Papyrus resolves/compiles a script by matching the two and rejects a mismatch at compile time. A Fallout 4-style namespaced name (e.g. ScriptName User:MyScript, stored at Scripts/Source/User/MyScript.psc) is compared by its final :-separated segment only, since the namespace itself is encoded as the script's containing subfolder rather than part of its file name. Only available when linting a file with a known path in the desktop app or CLI.

Property/variable named as script
script-name-collision
errorcorrectnessFlags a script-level Property or variable whose name matches the script it's declared in, since Papyrus fails to compile it.
Full behavior

Flags, as an [error], a script-level Property or variable whose name matches (case-insensitively) the name of the script it's declared in, since Papyrus rejects such a script at compile time. A local variable declared inside a function/event (see "Local variable shadowing" above) isn't checked by this lint.

Self-assignment
self-assignment
warningcorrectnessFlags a plain = assignment whose right side is the exact same reference as its own target (e.g. a = a, Self.Foo = Self.Foo), since it can never change the value and is almost always a mistake. Only a bare name or a chain of member accesses is compared this way; calls, indexes, and compound assignments (+=, ...) are never flagged.
Full behavior

Flags, as a [warning], a plain = assignment whose right-hand side is the exact same reference as its own target (e.g. a = a, Self.Foo = Self.Foo, akRef.Foo = akRef.Foo), since it can never change the value it reads and is almost always a copy-paste mistake or leftover from a refactor. Only a bare identifier or a chain of member accesses rooted at one (or at Self) is ever compared this way; a call, an index, or any other expression shape never counts as a self-assignment, since re-evaluating it on both sides of the same line isn't guaranteed to read the same value twice. A compound assignment (+=, -=, ...) is never flagged, since unlike plain = it does change the target's value.

Semicolon at end of line
semicolon
warningstyleRequires a trailing semicolon on each non-empty line or forbids terminal semicolons, according to the selected setting.
Full behavior

Requires, as a [warning], a trailing semicolon on each non-empty line or forbids terminal semicolons, according to the selected setting.

Repeated GlobalVariable.SetValue() calls in a loop
setvalue-in-loop
warningperformanceFlags a SetValue/SetValueInt call inside a While loop's body, since it then runs on every iteration; not flagged if the loop also calls Utility.Wait/RegisterForUpdate.
Full behavior

Flags, as a [warning], a SetValue/SetValueInt call standing alone as its own statement, directly in a While loop's body or nested inside an If/ElseIf/Else within it, since a call there runs on every iteration (or every reached iteration) the loop performs — likely unintended overhead compared to computing the final value locally and writing it once after the loop. Like "Repeated GlobalVariable.GetValue() calls", a call's receiver can't generally be resolved back to a GlobalVariable-typed script, so this matches by the SetValue/SetValueInt method name alone (case-insensitively). Never flagged when the loop's own body also calls Utility.Wait, RegisterForUpdate, RegisterForSingleUpdate, RegisterForUpdateGameTime, or RegisterForSingleUpdateGameTime, since that's a strong signal the write is deliberately paced rather than happening in a tight, uncontrolled loop. A nested While loop is checked as its own separate loop, so a wait call only inside it doesn't pace an outer loop's own write, and vice versa.

Short wait/update interval
short-wait-interval
warningperformanceFlags Waits and RegisterFor(Single)Update(GameTime) if the time is smaller than a configured amount.
Full behavior

Flags, as a [warning], a call to Utility.Wait, RegisterForUpdate, RegisterForSingleUpdate, RegisterForUpdateGameTime, or RegisterForSingleUpdateGameTime whose interval argument folds to a compile-time-constant number below the configurable min_wait_interval (default 0.1), since an interval that short runs far more often than is typically useful and can add up to meaningful performance overhead. Utility.Wait is only matched when qualified by that literal script name, the same way the "Forbidden/discouraged function usage" lint treats native singletons; the RegisterFor* family matches unqualified or through any receiver. Only an argument built entirely from literals (combined with arithmetic, comparison, logical, and unary operators) is checked; one that depends on an identifier, a call, Self/Parent, a member/index access, a cast, or a new array is left unflagged rather than guessed at.

Slow function usage
slow-functions
infoperformanceFlags calls to functions that have a faster equivalent available; the fix applies the supplied replacement.
Full behavior

Flags calls to functions listed in shared/rules/data/slow-functions.yaml that have a faster equivalent available, and suggests the quicker alternative. The fix replaces the complete call with that rule's supplied replacement, preserving the original argument where the replacement uses the value placeholder.

Stale compiled output
stale-compiled-output
infomaintainabilityFlags a .psc file whose compiled .pex is older than the script itself, usually meaning it was edited and never recompiled.
Full behavior

Flags, as an [info], a .psc file whose compiled .pex output (looked up at the conventional location alongside the source, e.g. Scripts/Example.pex for Scripts/Source/Example.psc) is older than the script itself — usually a sign the script was edited after it was last compiled. Only flagged when a .pex already exists at that location; a script that's never been compiled at all isn't flagged. Only available when linting a file with project context in the desktop app or CLI.

State function signature mismatch
state-function-signature
errorcorrectnessFlags a function or event in a State whose parameters or return type don't match its empty-state declaration.
Full behavior

Flags, as an [error], a function or event declared inside a State block whose parameter count/types or return type doesn't match the same-named declaration in the script's "empty state" (the one declared directly on the script, outside any State block) — Papyrus requires these to match identically for the state version to be recognized as an override of the empty-state one at all, rather than becoming a distinct, effectively unreachable function. Only compared against an empty-state declaration already present on the script being linted; a state function may instead validly match one declared on a parent script (per the language spec), which this lint has no way to resolve, so that case is left unflagged.

Static condition
static-condition
warningcorrectnessmaintainabilityFlags conditions that resolve the same every time due to this likely being a mistake.
Full behavior

Flags If/ElseIf/While conditions that fold to a constant true or false (e.g. If true, If 1 == 2, If !false && 3 > 4), regardless of any runtime state, as a [warning]. Only conditions built entirely from literals (combined with arithmetic, comparison, logical, and unary operators) are checked; one that depends on an identifier, a call, Self/Parent, a member/index access, a cast, or a new array is left unflagged rather than guessed at.

Static function called via instance
static-function-call-via-instance
warningmaintainabilityFlags a Global function called through an object reference (e.g. akRef.MyGlobalHelper()) instead of ScriptName.MyGlobalHelper(); Papyrus allows it, but it can read as a mistake. Self/Parent are never flagged.
Full behavior

Flags, as a [warning], the mirror case of "Non-static function call" above: a call reaching a Global function through an actual object reference (a local variable, parameter, property, cast, or array element, e.g. akOtherActor.MyGlobalHelper()) instead of Papyrus's static/global call syntax (MyScript.MyGlobalHelper()). Papyrus allows this — a Global function ignores whatever reference it's called through — but it can read as a mistake, since a reader (or the author, if the call was copy-pasted from an instance method) may expect the object to matter. Self/Parent are never flagged, since calling a script's own Global function through Self for symmetry with its other Self.Whatever() calls is a reasonable, common style rather than a likely mistake. Only checked when linting with project context, by resolving the target script's functions the same way the argument/return type checks do.

Strict boolean check
strict-boolean
warningcorrectnessstyleFlags conditions that rely on Papyrus treated a none-boolean as a boolean. Allows the literal 1/0 as bool-like by default (configurable).
Full behavior

Flags If/ElseIf/While conditions that aren't already a Bool value or expression, instead of relying on Papyrus's implicit conversion to boolean. Only conditions whose type can be determined locally (locals, parameters, properties, literals, casts, and comparison/logical expressions) are checked; a condition that depends on a function call or a member access is left unflagged rather than risk a false positive. By default (bool_like_int: true), the Int literal 1 or 0 used directly as a condition is allowed as a common "bool-like" idiom; any other Int value (including a variable or property that happens to hold 0/1) is still flagged, and setting bool_like_int: false flags the literals too.

Total named state count
too-many-named-states
errorcorrectnessFlags a file if the number of states exceeds the possible 128.
Full behavior

Flags, as an [error], a script whose named State blocks, combined with every State declared anywhere in its Extends ancestry (a same-named state declared more than once along the way counts once), exceed 127 — the CreationKit wiki's State Reference documents a hard engine limit of 128 states including the empty state, past which the game and CK refuse to load the script. Only the script's own declared states are counted when linting in isolation; when linting a .psc file dropped in the app, its Extends ancestry is resolved from the project root too, the same way the argument/return type checks resolve their own.

Trailing whitespace
trailing-whitespace
warningstyleFlags lines that end with trailing spaces or tabs.
Full behavior

Flags, as a [warning], lines that end with trailing spaces or tabs.

Type name casing
type-casing
warningstyleFlags a script's declared type name if it doesn't follow the configured convention. Ignores a leading acronym prefix (e.g. CreationKit's own fragment names or a mod's acronym prefix), which can't be renamed.
Full behavior

Flags, as a [warning], a script's declared type name (the identifier following ScriptName) if it doesn't follow the configured type_casing convention (PascalCase, camelCase, lowercase, or UPPERCASE). Only the script's own declared name is checked and fixed, never its Extends target, since that type is declared (and presumably already checked) in another script. Up to two leading acronym-prefix segments (one or more uppercase letters followed by one or more underscores, e.g. CreationKit's own IDR__TIF__050000F5 dialogue fragment names or a modder's own USSEP_ acronym prefix) are ignored, since that part of the name can't be renamed; only the rest of the name is checked and fixed. The fix only changes letter casing, preserving the name's characters so it remains compatible with its .psc filename; a violation that would require a substantive rename (such as removing an underscore for PascalCase) is left for the user to rename together with the file.

Array element used without a None check
unchecked-array-element
warningcorrectnessFlags a member/method access on a constant-indexed array element (e.g. act[2].Kill()) that hasn't yet been confirmed non-None in that path. Disabled by default, same as the Form parameter check above.
Full behavior

Flags, as a [warning], a member/method access (act[2].Kill()) on an element of a local array variable or array-typed parameter whose element type is a Form/script type, when that specific, constant-indexed element hasn't yet been confirmed non-None in that path, since nothing about declaring or sizing such an array guarantees any of its positions actually hold something. Tracks each element (identified by its array's name plus a constant-folded index, e.g. act[2]) as unconfirmed from the moment the array comes into scope until it's narrowed through If/ElseIf/Else branches guarded by a direct None check on that exact element (act[2] == None, act[2] != None, !act[2], a bare act[2], optionally combined with &&/||) or a While loop's condition, the same way "Form parameter used without a None check" narrows its own state, or until that element is assigned a new value, since the value just written could itself be None. Only a plain identifier's own element, indexed by a literal (optionally combined with arithmetic, comparison, logical, and unary operators), is tracked; a member/property array, or an index built from anything else, is left unflagged rather than guessed at. Passing the element on as an argument to another call isn't flagged, only a direct member/method access is. Disabled by default, for the same reason as "Form parameter used without a None check"; a project opts in via rules.unchecked_array_element.

Unchecked cast
unchecked-cast
warningcorrectnessFlags a member/method access on the result of an As cast before that result has been checked against None. Never flags CreationKit's own fragment boilerplate cast (e.g. Actor akSpeaker = akSpeakerRef as Actor).
Full behavior

Flags, as a [warning], a member/method access on the result of an as cast (e.g. (akRef as Actor).GetActorValue("Health")) before that result has been checked against None, since a cast that doesn't match the underlying Form's actual type evaluates to None at runtime rather than raising an error, so dereferencing it immediately crashes the script. Tracks a local variable as an unchecked cast result from its declaration/assignment from an as expression until it's reassigned something else, clearing it the moment a direct None check on it (x == None, x != None, !x, a bare x, optionally combined with &&/||) is evaluated, regardless of which branch is ultimately taken — this lint only cares whether the possibility of None was ever considered, not which branch handles it. A cast used directly inline ((value as Type).Member) is always flagged, since there's no way to check it in between. A cast CreationKit itself generated (the boilerplate line a quest/dialogue fragment gets between its Function signature and ;BEGIN CODE, e.g. Actor akSpeaker = akSpeakerRef as Actor) is never tracked as unchecked in the first place, since CreationKit guarantees that cast succeeds and the user can't add a None check there without CreationKit rejecting the edit.

Form parameter used without a None check
unchecked-form-parameter
warningcorrectnessFlags a member/method access on a Form-typed function parameter that hasn't yet been confirmed non-None in that path.
Full behavior

Flags, as a [warning], a member/method access (akForm.GetName()) on a Form-typed function parameter that hasn't yet been confirmed non-None in that path, since a caller can always pass in None and dereferencing it crashes the script at runtime. Tracks a parameter as unconfirmed from the start of its function until it's narrowed through If/ElseIf/Else branches guarded by a direct None check (x == None, x != None, !x, a bare x, optionally combined with &&/||) or a While loop's condition, the same way "None used as an existing Form" above narrows its own state, or until it's reassigned to anything else. A branch that unconditionally Returns doesn't carry its state past the If, covering the common If x == None / Return guard idiom. Passing the parameter on as an argument to another call isn't flagged, only a direct member/method access is. Disabled by default, since many scripts intentionally accept a possibly-None Form and defer the check to a caller or a later branch; a project opts in via rules.unchecked_form_parameter.

Unguarded self-recursion
unguarded-self-recursion
warningcorrectnessFlags a function/event that calls itself with nothing that could ever skip the recursive call, since it then recurses unconditionally until the call stack is exhausted. Conservative: a self-call nested inside an If/While is always left alone, a top-level While always disqualifies the function, and a top-level If only counts as a guard when it actually contains a Return somewhere within it — an If with no Return does nothing to stop the call and is still flagged. A GoToState to a different state that declares its own handler for the same function/event, before the self-call, counts as a guard too, covering the vanilla save-compatibility idiom. Exception: if every branch of an If/Elseif/Else covering every path directly calls itself, it's flagged anyway, since it recurses no matter which branch runs.
Full behavior

Flags, as a [warning], a function/event that calls itself with nothing that could ever skip the recursive call on some invocation, since without one it recurses unconditionally, every single time, until the call stack is exhausted. Deliberately conservative: it never follows a call chain through another function, and a self-call nested directly inside an If's or While's own body is always left alone, since being inside a branch or loop body already makes that call conditional. At the function's top level, any While still disqualifies the whole function from this lint, but a top-level If only counts as a guard — and so only disqualifies the function — when it actually contains a Return somewhere within it (searched recursively through any further nested If/While, since a guard's early exit can be buried behind further branching); an If with no Return anywhere inside it does nothing to actually stop the recursive call that follows it, so it's still flagged. This still doesn't evaluate whether a guard's condition is actually correct, only that a Return exists for it to possibly take. A self-call reached only through the right-hand side of a short-circuiting &&/|| doesn't count either, since that side isn't guaranteed to evaluate. A GoToState(...) to a different state that precedes the self-call in the same body also counts as a guard, but only when that other state actually declares its own handler for the same function/event — otherwise, dispatch would fall back to the empty state's declaration, which may still be this exact function, so the call is still flagged. This covers Bethesda's standard save-compatibility idiom, where a state switch right before the "recursive" call re-points dispatch at another (typically empty) handler instead of actually re-entering this body. One exception to a top-level If otherwise always being left alone: when it has an Else clause covering every path, and every one of its branches — each ElseIf and the final Else alike — directly contains a self-call among its own statements, those calls are flagged anyway, since no matter which branch runs the function calls itself again; a self-call buried inside a further-nested If/While within one of those branches still doesn't count towards it.

Unknown Actor Value
unknown-actor-value
warningcorrectnessFlags a call to an Actor Value function (GetActorValue, SetActorValue, ModActorValue, DamageActorValue, ...) whose Actor Value name isn't one of Skyrim's built-in ones, a strong sign of a typo. Only a plain string literal argument is checked. Disabled by default, since a project's own plugin can define custom Actor Values.
Full behavior

Flags, as a [warning], a call to an Actor Value function (GetActorValue, SetActorValue, ModActorValue, DamageActorValue, and the rest of that family) whose Actor Value name argument doesn't match one of Skyrim's built-in Actor Values, listed in shared/rules/data/actor-values.yaml — a strong signal of a typo. Matches by function name alone (case-insensitively), regardless of receiver, the same way "Forbidden/discouraged function usage" does; only a plain string literal argument is checked, one built from a variable or any other expression is left unflagged rather than guessed at. Disabled by default, since a project's own plugin can define additional, custom Actor Values that have no way to appear in shared/rules/data/actor-values.yaml; opt in with rules.unknown_actor_value.

Unnecessary function
unnecessary-function
infomaintainabilityperformanceFlags a Function whose body is just a single statement, since that's usually simple enough to inline at its call site instead. Events are never flagged, since they may deliberately forward to shared logic. A Function named Fragment_<digits> (matched case-insensitively; a CreationKit-generated fragment entry point) is never flagged either, since the engine calls it directly and it isn't something the script's own author could inline away. A parameterless Function returning Int, Float, Bool, or String is never flagged either, since that shape is how a script exposes a named constant to the outside world without baking it into every instance as a Property/Variable. The fix only handles a pure forwarding wrapper (its one statement is itself a call passing every one of its own parameters straight through, unchanged, to it), rewriting call sites to call the wrapped function directly; the wrapper's own declaration is left in place.
Full behavior

Flags, as an [info], a Function whose body consists of exactly one statement, since it adds an indirection without doing enough on its own to justify a separate declaration — a caller could just as well inline that one statement instead. Events are never flagged: they're declared by the engine rather than the script's own author, so a single-statement handler may well be forwarding to shared logic used by other events too. A Function named Fragment_<digits> (e.g. Fragment_0, matched case-insensitively) is never flagged either: CreationKit generates that name and calls it directly, so it isn't a wrapper the script's own author could inline away. A parameterless Function returning one of Papyrus's primitive scalar types (Int, Float, Bool, String, not an array and not an object/Form type) is never flagged either: that shape (e.g. Int Function GetFooThreshold() Global / Return 5 / EndFunction) is how a script exposes a named constant to the outside world while keeping the value out of every instance's own Property/Variable storage, which is a deliberate design choice rather than an indirection worth inlining away.

Unreachable elseif
unreachable-elseif
warningcorrectnessmaintainabilityFlags an ElseIf whose condition can never be true because an earlier branch already covers it.
Full behavior

Flags, as a [warning], an ElseIf branch whose condition can never be true because an earlier branch of the same If (the If itself or a prior ElseIf) already covers every value that would satisfy it (e.g. If x > 9 followed by ElseIf x > 10). Only a direct relational comparison (==, !=, <, <=, >, >=) between the exact same left-hand expression and a numeric literal is checked; a compound &&/|| condition, a non-numeric operand, or a differently-shaped left-hand expression is left unflagged rather than guessed at.

Unreachable statement
unreachable-statement
warningcorrectnessmaintainabilityFlags statements that follow a Return.
Full behavior

Flags statements that follow a Return within the same block (a function/event body, an If/ElseIf/Else branch, or a While body), since they can never execute.

Unresolved script reference
unresolved-script
warningcorrectnessFlags script name references that can't be resolved against the configured source directories.
Full behavior

Flags, as a [warning], an unresolved parent in Extends, an unresolved type annotation, or a call through Papyrus's static/global call syntax (e.g. MyMissingScript.DoThing()) whose target script can't be found. Primitive types and native engine types are recognized without project-side source. Only a call whose object is a bare identifier not already known as a local variable, parameter, or property is considered a script reference at all — one resolved through a variable or property is left to the "Argument type check"/"Return type check" lints instead. Only checked when linting with project context, by resolving names against .psc files under the project root the same way the argument/return type checks do, with native types and singleton scripts supplied by the built-in rule data.

Unused disable directive
unused-disable
warningmaintainabilityFlags unknown @disable/@disable-file rule ids and directives that suppress no diagnostic. Disabled by default.
Full behavior

Flags, as a [warning], each rule id in an @disable/@disable-file comment that is unknown or does not suppress a diagnostic from that rule (on its line for @disable, anywhere in the file for @disable-file). A bare @disable is flagged when its line has no diagnostics to suppress; a bare @disable-file is flagged when the whole file has none. Disabled by default; opt in with rules.unused_disable.

Getter usage without saving result
unused-getter
warningcorrectnessmaintainabilityFlags standalone calls to functions whose names begin with Get if the returned value is unused.
Full behavior

Flags calls to functions whose names begin with Get (case-insensitively) whose result is discarded, whether the call stands alone (GetValue()) or only feeds a comparison, arithmetic, or logical operator whose own result is then discarded too (e.g. GetDistance(target) > 0 on its own line, with no assignment, Return, or condition around it).

Unused import
unused-import
warningmaintainabilityFlags an Import statement whose script never has one of its Global functions called unqualified anywhere in this script. Only checked (and fixed) with project context.
Full behavior

Flags, as a [warning], an Import statement whose script never has one of its Global functions called unqualified anywhere in this script. Only checked when linting with project context, by resolving the imported script's functions the same way the argument/return type checks do; without that context, nothing is ever flagged rather than guessed at.

Unused or write-only local variables
unused-local-variable
warningmaintainabilityFlags a local variable inside a function/event) whose value is never read: either it's never referenced again at all.
Full behavior

Flags a local variable (declared with Type name = ... inside a function/event) whose value is never read: either it's never referenced again at all, or it's only ever reassigned (name = ...) without that new value ever being read back. Reading a variable via a compound assignment (name += ..., etc.) or through a member/index expression built from it (name.Foo, name[0]) counts as a use. Function parameters and script properties aren't locals and are never flagged by this lint.

Discarded nodiscard result
unused-nodiscard
warningcorrectnessmaintainabilityFlags discarded results of functions marked ; @nodiscard.
Full behavior

Flags calls to functions marked ; @nodiscard (on the function header or the comment line immediately above it) whose result is discarded, whether the call stands alone (RegisterFoo()) or only feeds a comparison, arithmetic, or logical operator whose own result is then discarded too (e.g. RegisterFoo() > 0 on its own line, with no assignment, Return, or condition around it). Distinct from unused-getter, which keys off the Get* name prefix; both can fire on the same call if a getter is also marked @nodiscard.

Unused script properties
unused-property
warningmaintainabilityFlags Property declarations whose name is never referenced anywhere else in the script.
Full behavior

Flags Property declarations whose name is never referenced anywhere else in the script.

Useless downcast
useless-downcast
infomaintainabilityFlags a cast to a parent type that has no actual value and just makes reading a bit harder.
Full behavior

Flags, as an [info], an explicit as cast that can't actually narrow anything: either its target type exactly matches the value's already-known type, or the value's type already extends the target (directly or transitively) — e.g. Actor dude followed by Foo(dude as ObjectReference), since Actor already extends ObjectReference and Papyrus would accept dude there without the cast. Only a cast whose value's type can be determined locally (locals, parameters, properties, Self/Parent, literals, and other resolvable expressions) is checked; a member access or function call result is left unflagged rather than guessed at. Primitive types (Int, Float, Bool, String) are only flagged for an exact-type cast, never treated as extending one another, so a meaningful conversion like an explicit Int-to-Float widening cast is never flagged. When linting a .psc file dropped in the app, a cast target that's an ancestor of the value's script (rather than an exact match) is resolved the same way the argument/return type checks resolve their own, including engine types like Actor/ObjectReference/Form resolved from the configured script roots.

Local variable used before assignment
variable-used-before-assignment
warningcorrectnessFlags a local variable declared without an initial value that's read before it's ever assigned one, since it then still holds its default value. A comparison against that same default (None, 0, 0.0, False, or "") is treated as a deliberate gate, not a flagged read, even when joined by && or || to a further operand that only runs once the gate rules out the default.
Full behavior

Flags, as a [warning], a read of a local variable declared without an initial value (Int i rather than Int i = 0) before anything in the enclosing function ever assigns it one, since the read then actually observes Papyrus's implicit per-type default (0, 0.0, False, "", or None) rather than a value the author chose. A variable is tracked as unassigned from its declaration until a plain name = value assignment reaches it; a compound assignment (name += 1, ...) is flagged too, since it reads the still-default value before writing the new one, and then counts as assigned from that point on. If/ElseIf/Else branches are each checked from the same incoming state, and a branch that unconditionally Returns doesn't carry its state past the If; a variable assigned by any surviving branch counts as assigned afterward too, so a variable assigned in only one branch (or with no Else at all) and later tested against its default to see whether that branch ran isn't flagged — only a variable left unassigned by every surviving branch still is. Since a While loop may run zero times, an assignment made only inside its body is never assumed to have happened by the time execution reaches the code after the loop. Function parameters and script properties always have a value by the time a function runs and are never flagged by this lint. An ==/!= comparison against that same variable's own declared-type default (None, 0, 0.0, False, or "") is treated as a deliberate "has this been set yet?" gate rather than a genuine read, so it's never flagged either — a mismatched comparison like Int i against None, or Bool b against 0, isn't that type's default and is still flagged (other reads of the same variable still are too). That gate exemption also carries across a short-circuiting &&/|| joining it to a further operand (e.g. x == None || x.Foo(), or x != None && x.Foo()), since that operand only ever evaluates once the gate has established the variable is no longer at its default.

Add a trailing ; @disable <rule-id>[, <rule-id>...] comment to a line to suppress diagnostics from those rules on that line only (e.g. action = 1 ; @disable float-to-int); a bare ; @disable suppresses every rule on the line. A ; @disable-file <rule-id>[, <rule-id>...] comment does the same across the whole file instead, no matter which line it's written on. See the README for more details.