declint-core 1.0.0

Config and regex linting engine for declint: YAML rule files, message templates, violations. No LSP dependencies.
Documentation
# declint preset: Python
#
# A curated starter ruleset — the rules from doc/python-tutorial.md.
# Import it from your config:
#
#   import:
#     - preset:python
#
# Presets never declare `languages` — set that in your own config:
#
#   languages: [python]
#
# Every rule here can be overridden where it collides is not supported:
# ids must be unique, so to customize a rule, copy it out of the preset
# and adjust the copy.

version: 1

rules:
  - id: no-tabs
    pattern: '(?m)^\t+'
    message: "PEP 8: spaces, not tabs, for indentation"
    severity: warning
    tests:
      - name: tab indentation is flagged
        text: "\tindented"
        violations: 1
      - name: clean lines pass
        text: "indented"
        violations: 0

  - id: no-semicolons
    pattern: '(?m)^\s*[^#\s].*;\s*$'
    message: "PEP 8: avoid semicolons to separate statements"
    severity: warning

  - id: no-wildcard-imports
    pattern: '(?m)^\s*from\s+[\w.]+\s+import\s+\*'
    message: "wildcard imports make names ambiguous"
    severity: warning

  - id: bare-except
    pattern: '(?m)^\s*except\s*:'
    message: "bare except catches everything — name the exception"
    severity: warning

  - id: compare-to-none
    pattern: '==\s*None\b'
    message: "use 'is None', not '== None'"
    severity: warning

  - id: assignment-in-condition
    pattern: '(?m)^\s*(?:if|elif|while)\s+[^#=]*[^=!<>]=[^=]'
    message: "single '=' assigns — did you mean '=='?"
    severity: error

  - id: mutable-default
    pattern: '(?m)^\s*def\s+(?<function>\w+)\s*\([^)]*=[ \t]*(?<default>\[\]|\{\})(?<rest>[^\n]*)'
    severity: error
    callback: |
      return function(c)
        if c.captures.rest:find("declint:allow") then
          return nil
        end
        return { message = "default " .. c.captures["default"] .. " in '" ..
                 c.captures["function"] .. "' is shared across calls" }
      end
    tests:
      - name: shared default is flagged
        text: "def f(a = []):"
        violations: 1
        messages: ["default [] in 'f' is shared across calls"]
      - name: allow marker vetoes
        text: "def f(a = []):  # declint:allow"
        violations: 0

scopes:
  # A top-level function — including `async def`. The region ends at the
  # next line that starts with any non-whitespace character (another
  # def/class, a decorator, or plain top-level code).
  - id: function
    start: '^(?:async\s+)?def\s'
    end: '^\S'
    rules:
      - id: print-in-function
        pattern: '\bprint\('
        message: "leftover print() — remove or use logging"
        severity: warning
        tests:
          - name: print inside a function is flagged
            text: "def f():\n    print(1)\n"
            violations: 1
          - name: top-level print is allowed
            text: "print(1)\n"
            violations: 0