← All docs

Examples

The Simple Example in the README shows one way Papyrus Lint catches a bug PapyrusCompiler.exe compiles without complaint. The examples below show more of the same kind — real bugs, not style nitpicks — pulled from the Reliability, Bugprone, and Performance categories of the lint rule reference. Every snippet compiles cleanly; the linter is what actually catches the problem.

The Simple Example in the README shows one way Papyrus Lint catches a bug PapyrusCompiler.exe compiles without complaint. The examples below show more of the same kind — real bugs, not style nitpicks — pulled from the Reliability, Bugprone, and Performance categories of the lint rule reference. Every snippet compiles cleanly; the linter is what actually catches the problem.

Dereferencing a None Form

ScriptName Example extends ObjectReference

Function GreetOwner(Actor akOwner = None)
    Actor a = None
    a.SendAnimationEvent("wave")
EndFunction

a is declared None and never assigned anything else before it's used, so a.SendAnimationEvent(...) crashes the script the moment this function runs. The None used as an existing Form lint (none-form-usage) flags the access; the likely fix is to assign a before using it, or to guard the call:

ScriptName Example extends ObjectReference

Function GreetOwner(Actor akOwner = None)
    Actor a = akOwner
    If a != None
        a.SendAnimationEvent("wave")
    EndIf
EndFunction

Division by zero

ScriptName Example

Function GetAverage(Int total, Int[] values)
    Int average = total / (values.Length - values.Length)
EndFunction

values.Length - values.Length always folds to 0, so this division always crashes the script at runtime — the compiler has no reason to reject it, since dividing by zero is only ever a problem once the script actually runs. The Division by zero lint (division-by-zero) flags any divisor that folds to a compile-time-constant zero.

Array index out of bounds

ScriptName Example

Function ResetSlots()
    Int[] slots = new Int[3]
    slots[3] = 0
EndFunction

slots was created with 3 elements, so valid indexes are 0 through 2slots[3] is out of range. Papyrus doesn't raise a catchable error for this; it silently no-ops the write, so slots[3] = 0 quietly does nothing at runtime instead of failing loudly. The Array bounds lint (array-bounds) tracks the declared size of new-created arrays and flags a literal index that falls outside it.

Wrong return type

ScriptName Example

Bool Function HasEnoughGold(Actor akActor, Int amount)
    Return akActor.GetItemCount(Gold001)
EndFunction

GetItemCount() returns an Int, but HasEnoughGold is declared to return a Bool — Papyrus implicitly converts the count to a boolean (0 becomes False, anything else becomes True), which almost certainly isn't the actual comparison the author meant to make. The Return type check lint (return-types) flags the mismatched Return; the likely fix is an explicit comparison:

ScriptName Example

Bool Function HasEnoughGold(Actor akActor, Int amount)
    Return akActor.GetItemCount(Gold001) >= amount
EndFunction

Integer division silently truncating

ScriptName Example

Function ApplyDiscount(Int price, Int discountPercent)
    Float remainingFraction = (100 - discountPercent) / 100
    Debug.Notification("You pay " + (price * remainingFraction))
EndFunction

Both (100 - discountPercent) and 100 are Ints, so Papyrus performs Int / Int division — truncating toward zero — *before* the result widens into the Float-typed remainingFraction. A 25% discount (75 / 100) becomes 0, not 0.75, so every customer pays nothing. The Int/Int division widened to Float lint (int-division-to-float) flags this; casting an operand (not the result) fixes it:

ScriptName Example

Function ApplyDiscount(Int price, Int discountPercent)
    Float remainingFraction = (100 - discountPercent) / 100 as Float
    Debug.Notification("You pay " + (price * remainingFraction))
EndFunction

Unguarded self-recursion

ScriptName Example

Function CountDown(Int seconds)
    Debug.Notification(seconds as String)
    Utility.Wait(1.0)
    CountDown(seconds - 1)
EndFunction

Nothing here ever stops CountDown from calling itself again, so it recurses unconditionally on every invocation until the call stack is exhausted. The Unguarded self-recursion lint (unguarded-self-recursion) flags a self-call with no guard that could ever skip it; a Return guard fixes it:

ScriptName Example

Function CountDown(Int seconds)
    If seconds <= 0
        Return
    EndIf
    Debug.Notification(seconds as String)
    Utility.Wait(1.0)
    CountDown(seconds - 1)
EndFunction

Self-assignment

ScriptName Example

Function Reset(Int startingHealth)
    Int health = startingHealth
    health = health
    Debug.Notification("Health reset to " + health)
EndFunction

health = health reads and writes the exact same value, so it has no effect at all — a leftover from a refactor, most likely a dropped right-hand side that was meant to compute something else. The Self-assignment lint (self-assignment) flags it; the fix is either to remove the pointless line or to assign the value that was actually intended:

ScriptName Example

Function Reset(Int startingHealth)
    Int health = startingHealth
    Debug.Notification("Health reset to " + health)
EndFunction

Short update interval

ScriptName Example extends ObjectReference

Event OnInit()
    RegisterForSingleUpdate(0.01)
EndEvent

Event OnUpdate()
    ; ...
    RegisterForSingleUpdate(0.01)
EndEvent

A 0.01-second update interval runs far more often than is typically useful, and adds up to meaningful performance overhead across many active references. The Short wait/update interval lint (short-wait-interval) flags a RegisterForSingleUpdate/RegisterForUpdate/ Utility.Wait interval below the configurable min_wait_interval (default 0.1).

Discarded getter result

ScriptName Example extends ObjectReference

Function CheckDistance(Actor akTarget)
    GetDistance(akTarget) > 500.0
EndFunction

The comparison's result is never assigned, returned, or used in a condition — it's computed and immediately thrown away, which is almost always a forgotten If rather than something intentional. The Getter usage without saving result lint (unused-getter) flags a Get* call (or an expression built from one) whose result is discarded this way.

The Discarded nodiscard result lint (unused-nodiscard) is the same idea for functions the author marked ; @nodiscard rather than ones named Get*. A discarded RegisterFoo() whose header carries ; @nodiscard is flagged even though the name is not a getter; a used result (Int id = RegisterFoo()) is not.

ScriptName Example

Int Function RegisterFoo() ; @nodiscard
    Return 1
EndFunction

Function Test()
    RegisterFoo()
EndFunction

Silencing a lint for code that's already correct

ScriptName Example extends Quest

Actor Property playerRef Auto

Function GetPlayer()
    If ! playerRef
        playerRef = Game.GetPlayer()
    EndIf
    Return playerRef
EndFunction

The Forbidden/discouraged function usage lint (forbidden-functions) flags every call to Game.GetPlayer() as an [error], since shared/rules/data/forbidden-functions.yaml recommends caching the result in an Actor Property PlayerRef Auto instead of calling it repeatedly. This function already does exactly that — playerRef is only assigned once, the first time it's still unset — but the lint can't see that the call is guarded, so it still fires on the one call this idiom actually makes. Rather than rewriting the very caching pattern the lint's own message recommends just to dodge a false positive, silence that one line with a trailing ; @disable forbidden-functions comment instead:

ScriptName Example extends Quest

Actor Property playerRef Auto

Function GetPlayer()
    If ! playerRef
        playerRef = Game.GetPlayer() ; @disable forbidden-functions
    EndIf
    Return playerRef
EndFunction

---

See the lint rule reference for the full list, including the purely style/formatting lints not shown here, and Disabling a lint on a specific line for the full ; @disable/; @disable-file directive syntax.

View raw source on GitHub →