Module: Moult::FlagScanner

Defined in:
lib/moult/flag_scanner.rb

Overview

The OpenFeature flag-evaluation SCANNER — the only file that knows the OpenFeature client call shape, so a future SDK or provider shift is a swap, not a rewrite (the same isolation Clones gives flay and Boundaries::Packwerk gives packwerk). It is a pure Prism scan over source, shaped like SymbolScanner: there is no external-tool output to ingest here.

OpenFeature (github.com/open-feature/ruby-sdk, gem openfeature-sdk) is the provider-agnostic feature-flag standard: a client is built via OpenFeature::SDK.build_client and flags are evaluated with client.fetch_<type>_value(flag_key:, default_value:, evaluation_context:) (and the fetch_<type>_details variants). Scanning that client surface catches flag usage behind any provider (flagd, LaunchDarkly, GO Feature Flag, ...).

We detect by AST only and take NO dependency on the openfeature-sdk gem — we read the call shape, we never call the SDK. A call is an OpenFeature evaluation when its method name is one of the known fetch_* names AND it passes a flag_key: keyword argument (the keyword uniquely disambiguates it from any unrelated same-named method, since the receiver is a runtime value).

Defined Under Namespace

Classes: CallSite, Visitor

Constant Summary collapse

TARGET =

Provenance recorded in the report's analysis.scanner block. The swap point: retarget these (and METHOD_VALUE_TYPES) for a different SDK/standard.

"openfeature"
SDK_GEM =
"openfeature-sdk"
CLIENT_BUILDER =
"OpenFeature::SDK.build_client"
FETCH_TYPES =

The fetch__(value|details) method names mapped to the contract's value_type. integer/float collapse to "number" (the value_type enum is coarser than the SDK's fetch types); the precise method name is kept on each call site so nothing is lost.

{
  "boolean" => "boolean",
  "string" => "string",
  "number" => "number",
  "integer" => "number",
  "float" => "number",
  "object" => "object"
}.freeze
METHOD_VALUE_TYPES =
FETCH_TYPES.each_with_object({}) do |(fetch_type, value_type), acc|
  acc["fetch_#{fetch_type}_value"] = value_type
  acc["fetch_#{fetch_type}_details"] = value_type
end.freeze
LITERAL_NODES =

The literal default_value node types we render. A non-literal default (a variable, method call, array/hash) renders to nil — recorded as "no observed literal default" rather than guessed.

[
  Prism::StringNode, Prism::SymbolNode, Prism::IntegerNode,
  Prism::FloatNode, Prism::TrueNode, Prism::FalseNode, Prism::NilNode
].freeze

Class Method Summary collapse

Class Method Details

.scan_file(path, rel_path) ⇒ Array<CallSite>

Parameters:

  • path (String)

    file to read

  • rel_path (String)

    root-relative path stamped onto each call site

Returns:



67
68
69
# File 'lib/moult/flag_scanner.rb', line 67

def scan_file(path, rel_path)
  scan_source(File.read(path), rel_path)
end

.scan_source(source, path) ⇒ Array<CallSite>

Parameters:

  • source (String)

    Ruby source

  • path (String)

    path stamped onto each call site

Returns:



74
75
76
77
78
79
# File 'lib/moult/flag_scanner.rb', line 74

def scan_source(source, path)
  result = Prism.parse(source)
  visitor = Visitor.new(path)
  result.value.accept(visitor)
  visitor.call_sites
end