Class: Audition::Static::Checks::Base

Inherits:
Prism::Visitor
  • Object
show all
Defined in:
lib/audition/static/checks/base.rb

Overview

A check is a Prism visitor plus a message catalog, written in a small declarative DSL:

class MyCheck < Base
check_name "my-check"           # optional; derived

explain :bad_thing,
        severity: :error,
        message: "found %{name}",
        why: "it breaks because ...",
        fix: "do this instead ..."

on :call_node do |node|
  flag(node, :bad_thing, name: node.name)
end
end

on generates the visit method and always continues into child nodes, so handlers cannot accidentally stop traversal. Third-party checks plug in via Checks.register(MyCheck).

Constant Summary collapse

EMPTY_CATALOG =
{}.freeze
WRAPPER =

Methods born from define_method carry their block as a Proc, and Ruby refuses to call them from another Ractor unless that Proc is shareable. Both the handler and the generated wrapper are therefore isolated via Ractor.make_shareable; the wrapper looks its handler up through method and continues traversal explicitly (super is not available inside an isolated proc, and this reproduces Prism::Visitor's default body).

Ractor.make_shareable(
  proc do |node|
    handler_for(__method__, node)
    node.each_child_node { |child| child.accept(self) }
  end
)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file) ⇒ Base

Returns a new instance of Base.



121
122
123
124
125
# File 'lib/audition/static/checks/base.rb', line 121

def initialize(file)
  @file = file
  @findings = []
  super()
end

Instance Attribute Details

#fileObject (readonly)

Returns the value of attribute file.



119
120
121
# File 'lib/audition/static/checks/base.rb', line 119

def file
  @file
end

#findingsObject (readonly)

Returns the value of attribute findings.



119
120
121
# File 'lib/audition/static/checks/base.rb', line 119

def findings
  @findings
end

Class Method Details

.call(file) ⇒ Array<Finding>

Runs this check over one parsed file.

Parameters:

Returns:



112
113
114
115
116
# File 'lib/audition/static/checks/base.rb', line 112

def call(file)
  visitor = new(file)
  visitor.visit(file.root)
  visitor.findings
end

.check_name(value = nil) ⇒ String

No lazy memoization here: checks must be readable from non-main Ractors (parallel scanning), so class-level state is written eagerly on the main Ractor at class definition time and kept deeply shareable.

Parameters:

  • value (String, nil) (defaults to: nil)

    sets the identifier when given; otherwise derived from the class name

Returns:

  • (String)

    kebab-case check identifier



40
41
42
43
44
# File 'lib/audition/static/checks/base.rb', line 40

def check_name(value = nil)
  @check_name = -value if value # audition:disable class-level-state
  @check_name || name.split("::").last
    .gsub(/([a-z0-9])([A-Z])/, '\1-\2').downcase
end

.explain(key, severity:, message:, why:, fix:) ⇒ void

This method returns an undefined value.

Registers a message catalog entry. Strings may contain %{placeholders} filled by #flag.

Parameters:

  • key (Symbol)

    catalog key used by #flag

  • severity (Symbol)

    :error, :warning, :info

  • message (String)

    one-line problem statement

  • why (String)

    the Ractor rule behind it

  • fix (String)

    suggested remediation



59
60
61
62
63
64
65
66
67
# File 'lib/audition/static/checks/base.rb', line 59

def explain(key, severity:, message:, why:, fix:)
  entry = {
    severity: severity, message: message,
    why: why, fix: fix
  }
  @explanations = Ractor.make_shareable( # audition:disable
    explanations.merge(key => entry)
  )
end

.explanationsObject



46
47
48
# File 'lib/audition/static/checks/base.rb', line 46

def explanations
  @explanations || EMPTY_CATALOG
end

.handlersObject



69
70
71
# File 'lib/audition/static/checks/base.rb', line 69

def handlers
  @handlers || EMPTY_CATALOG
end

.on(*node_types) {|node| ... } ⇒ void

This method returns an undefined value.

Generates visit methods for the given Prism node types. The handler runs via instance_exec and traversal always continues into child nodes afterwards. Handlers must not capture outer locals (they are isolated for cross-Ractor use and Ractor.make_shareable would raise).

Parameters:

  • node_types (Array<Symbol>)

    e.g. :call_node

Yield Parameters:

  • node (Prism::Node)

    the visited node



97
98
99
100
101
102
103
104
105
106
# File 'lib/audition/static/checks/base.rb', line 97

def on(*node_types, &handler)
  isolated = Ractor.make_shareable(handler)
  node_types.each do |type|
    method_name = :"visit_#{type}"
    @handlers = Ractor.make_shareable( # audition:disable
      handlers.merge(method_name => isolated)
    )
    define_method(method_name, &WRAPPER)
  end
end