Linter

PineLinter static rules: syntax via parse, version, deprecations, naming, style codes.

This page

Linter

The core linter is a lightweight static checker that sits on the parse pipeline plus regex rules. It is not a full dataflow analyzer; it is the first automated gate for “does this even look like modern Pine?”

Abstract

src/pynescript/ast/linter.py exports:

  • LintWarning — dataclass (code, message, line, column, severity)
  • PineLinter — stateful runner accumulating warnings
  • lint_script(source, filename) / lint_file(filepath) — convenience entry points

Rules run in fixed order: syntax → version → deprecated patterns → naming → style. Syntax failures become severity "error" with code E001; other rules are mostly "warning".

Conceptual model

Diagram

Rendering…

Interface surface

from pynescript.ast.linter import lint_script, PineLinter, LintWarning

warnings = lint_script("""
indicator("x")
plot(close)
""")
for w in warnings:
    print(w)  # warning: [W001] Missing @version ... at line 1

LintWarning

FieldTypeMeaning
codestrStable id (E001, W001, C002, …)
messagestrHuman-readable explanation
lineint | None1-based when known
columnint | NoneReserved / optional
severitystr"warning" (default) or "error"

__str__ format: {severity}: [{code}] {message} at {location}.

PineLinter.lint(source, filename="<input>") -> list[LintWarning]

Resets self.warnings, runs all checks, returns the list (also stored on the instance).

File helper

from pynescript.ast.linter import lint_file

issues = lint_file("strategies/mean_reversion.pine")

Reads UTF-8 text then delegates to lint_script.

Rule catalog

Syntax

CodeSeverityCondition
E001errorparse(source, filename) raises any exception

Does not attempt recovery; one syntax error check per run. Location is taken from pynescript.ast.error.SyntaxError.details (lineno, offset) when present; otherwise a line N regex on the exception text. The warning message prefers the short .message attribute so caret dumps do not land in chips.

Version

CodeCondition
W001No //@version = N (flexible whitespace) match
W002Version integer &lt; 5 (deprecated; suggest v6)

Pattern: //\s*@version\s*=\s*(\d+).

Deprecated patterns (_check_deprecated)

CodePattern (simplified)Advice
W101security('EXCHANGE:SYM'…) stylePrefer request.security() with explicit params
W102(retired) plot(… style=plot.style_histogramWas: consider plotcandle. Code kept greppable; the rule no longer fires.
W103(retired) var int name = naWas: prefer 0 for type safety. Code kept greppable; the rule no longer fires.

Matches set line from prefix newline count. Case-insensitive search. Live deprecated rule is W101 only.

Naming (_check_naming)

CodeCondition
C001LHS of name = ta.… starts with lowercase — message suggests camelCase via _to_camel

Heuristic only: line-local regex (\w+)\s*=\s*ta\..

Style (_check_style)

CodeCondition
C002Line length (rstrip) > 120
C003(retired) Line matches ^\s+if\s+ — was “single-line if without braces”; code kept greppable
C004File does not end with newline

Internals

Path

src/pynescript/ast/linter.py

Dependencies:

  • pynescript.ast.parse (re-exported path via from pynescript.ast import parse — subject to the helper sha256 LRU)
  • Standard library re, dataclasses

Design stance

The linter intentionally uses regex over AST for several rules so it still partially works when parse fails (version/deprecations/style still run after a failed syntax check — note: _check_syntax records E001 but does not abort the pipeline). That means:

  • False positives/negatives on fancy formatting are possible.
  • Deep semantic issues (wrong series type, undefined names) belong to evaluator/LSP diagnostics, not these codes.

Integration points

CLI, editor save hooks, and CI can call lint_script without spinning up the full evaluator. LSP diagnostics may layer additional semantic checks beyond this module.

Invariants

  1. lint() always clears prior warnings on that instance before running.
  2. Codes are stable public strings — treat renames as breaking for tooling.
  3. Syntax errors are not fatal to the function — you get E001 plus any later regex hits.
  4. No mutation of source — pure analysis.

Worked examples

Clean modern script

from pynescript.ast.linter import lint_script

src = """//@version=6
indicator("ok")
length = 14
basis = ta.sma(close, length)
plot(basis)
"""
assert lint_script(src) == []  # may still flag C001 if naming heuristic fires

Note: basis = ta.sma(...) triggers C001 under the current rule (lowercase LHS). Prefer documenting that heuristic when teaching style.

Missing version

ws = lint_script('indicator("x")\nplot(close)\n')
assert any(w.code == "W001" for w in ws)

Programmatic filter

errors = [w for w in lint_script(src) if w.severity == "error"]
if errors:
    raise SystemExit(1)

Failure modes

IssueExplanation
C003 on legitimate indented if blocksRule retired in 0.6.0; the code no longer fires (was a coarse ^\s+if\s+ heuristic)
C001 noiseMany valid snake_case or lower identifiers
E001 still not a caret dumpLinter stores the short .message plus line/column; catch pynescript.ast.error.SyntaxError for the full caret __str__
Encoding errors in lint_fileNon-UTF-8 files raise at read time
False security deprecationPattern looks for quoted EXCHANGE:SYM form only

See also