declint-core 1.3.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: except-pass
    pattern: '\bexcept\b[^:]*:\s*pass\b'
    message: "swallowed exception — at least log it"
    severity: warning
    tests:
      - name: except-pass is flagged
        text: "try:\n    x\nexcept ValueError:\n    pass\n"
        violations: 1
      - name: logged exception passes
        text: "try:\n    x\nexcept ValueError as e:\n    log(e)\n"
        violations: 0

  - id: open-without-with
    pattern: '=\s*open\('
    message: "open() without a with block leaks the handle"
    severity: warning
    tests:
      - name: bare open() is flagged
        text: "f = open(path)\n"
        violations: 1
      - name: with-open passes
        text: "with open(path) as f:\n    pass\n"
        violations: 0

  - id: type-ignore
    pattern: '#\s*type:\s*ignore'
    message: "type: ignore — leave a comment saying why"
    severity: info

  - 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

      # Absence rule (parser): a function region without a docstring.
      # Delete this rule if docstrings are not your house style.
      - id: missing-docstring
        parser: |
          return function(text, offset)
            if text:find('"""') then
              return nil
            end
            local first = text:match('^[^\n]*')
            return { { start = offset, finish = offset + #first } }
          end
        message: "no docstring — say what this function does"
        severity: info
        tests:
          - name: undocumented function is suggested
            text: "def f():\n    pass\n"
            violations: 1
          - name: documented function passes
            text: "def f():\n    \"\"\"Does a thing.\"\"\"\n"
            violations: 0

  # A top-level class, same region logic as functions — with the same
  # docstring rule.
  - id: class
    start: '^class\s'
    end: '^\S'
    rules:
      - id: missing-class-docstring
        parser: |
          return function(text, offset)
            if text:find('"""') then
              return nil
            end
            local first = text:match('^[^\n]*')
            return { { start = offset, finish = offset + #first } }
          end
        message: "no docstring — say what this class does"
        severity: info