class Analyzer::Ruby::Cli

Overview

Surfaces the command-line attack surface of Ruby programs as cli:// endpoints: one endpoint per (sub)command, with named options (param_type "flag"), positional arguments ("argument") and consumed environment variables ("env"). Covers stdlib OptionParser / ARGV plus Thor, GLI, Slop, TTY::Option, the commander gem, Optimist, Clamp and dry-cli.

Line-scan analyzer (house style for non-tree-sitter Ruby adapters), merging endpoints by URL across files.

Defined in:

analyzer/analyzers/ruby/cli.cr

Constant Summary

ARGV_INDEX = /\bARGV\s*\[\s*(\d+)\s*\]/

builtin argv / env.

BLOCK_COMMAND = /^\s*command\s+:?["']?([A-Za-z0-9][\w-]*)/

GLI / commander gem command blocks + flags.

CLAMP_OPTION_LONG = /^\s*option\s+(?=["'\[])[^\n]*?["'](-{2}[A-Za-z0-9][\w-]*)["']/
CLAMP_OPTION_SHORT = /^\s*option\s+(?=["'\[])[^\n]*?["'](-[A-Za-z0-9])["']/
CLAMP_PARAMETER = /^\s*parameter\s+["']\[?([A-Za-z0-9_]+)/
CLAMP_SUBCLASS = /<\s*Clamp::Command\b/

Clamp: class Foo < Clamp::Command with option/parameter DSL and optional nested subcommand "name", "desc" do ... end blocks. The DSL call's first argument (the switch name or an array of switch names) must appear immediately after option + whitespace — a (?=["'\[]) lookahead — so an unrelated local/instance variable assignment like option = default? ? "--json" : "--text" cannot masquerade as the DSL call just because a dash-prefixed quoted string appears somewhere later on the line. Mirrors CLAMP_PARAMETER, which already requires the quote directly after the keyword.

CLAMP_SUBCOMMAND = /^(\s*)subcommand\s+["']([A-Za-z0-9][\w-]*)["'][^\n]*\bdo\s*$/
CLI_EVIDENCE_MARKERS_RE = Regex.union("OptionParser.new", "GLI::App", "TTY::Option", "Commander::Methods")

cli_evidence? runs once per .rb file and OR-ed four String#includes? scans as a standalone boolean gate. Folded into one precompiled union so the literal-marker half of the gate costs a single PCRE2 match instead of up to four naive substring scans.

COMMANDER_OPT = /\bc\.option\s+[^\n]*?["'](-{2}[A-Za-z0-9][\w-]*)/
COMMANDER_PROGRAM = /\bprogram\s+:name\s*,\s*["']([^"']+)["']/

Program-name hints.

DEF_RE = /^\s*def\s+([a-z_]\w*[?!]?)\s*(?:[(\s]|$)/

Instance methods only: def self.exit_on_failure? (standard Thor boilerplate) is a class method, never a command, so the name must be followed by an argument list, whitespace or end-of-line — not ..

DRY_CLI_ARGUMENT = /^\s*argument\s+:([A-Za-z0-9_]+)/
DRY_CLI_CLASS = /^(\s*)class\s+([A-Za-z0-9_]+)\s*<\s*Dry::CLI::Command\b/
DRY_CLI_MARKER = /<\s*Dry::CLI::Command\b/

dry-cli: class Build < Dry::CLI::Command with option/argument DSL; each subclass is its own (sub)command. DRY_CLI_MARKER (unanchored) is for whole-content evidence checks; DRY_CLI_CLASS (line-anchored, to capture indent) is for the per-line scan — ^ in Crystal only matches the very start of a multi-line string, not after every \n.

DRY_CLI_OPTION = /^\s*option\s+:([A-Za-z0-9_]+)/
ENV_FETCH = /\bENV\.fetch\s*\(\s*["']([^"']+)["']/
ENV_INDEX = /\bENV\s*\[\s*["']([^"']+)["']\s*\]/
GLI_FLAG = /^\s*(?:flag|switch)\s+:?["']?([A-Za-z0-9][\w-]*)/
OPTIMIST_CALL = /\bOptimist(?:::|\.)options\b/

Optimist: opt :name, "desc", type: :string — a flat parser (no subcommand DSL), gated on the block-opening call so a bare local variable/method named opt elsewhere doesn't false-positive.

OPTIMIST_OPT = /^\s*opt\s+:([A-Za-z0-9_]+)/
OPTPARSE_BANNER = /banner\s*=\s*["']Usage:\s*(\S+)/
OPTPARSE_LONG = /\.on\s*\(?[^)]*?["'](-{2}[A-Za-z0-9][\w-]*)/

OptionParser: opts.on("-p", "--port PORT") — prefer the long name.

OPTPARSE_SHORT = /\.on\s*\(\s*["'](-[A-Za-z0-9])(["' ]|\))/
SLOP_LONG = /\bo\.(?:string|integer|int|float|bool|boolean|array|symbol|null|on)\s+[^\n]*?["'](-{2}[A-Za-z0-9][\w-]*)/

Slop: o.string '-p', '--port'.

THOR_DESC = /^\s*desc\s+["']([^"'\s]+)/
THOR_OPTION = /^\s*(?:method_option|option)\s+:?["']?([A-Za-z0-9][\w-]*)/
THOR_SUBCLASS = /<\s*Thor\b/

Thor DSL.

TTY_ARGUMENT_DECL = /^\s*argument\s+:?(\w+)/
TTY_OPTION_DECL = /^\s*(?:option|flag|keyword)\s+:?(\w+)/

TTY::Option DSL (only when no Thor class is present, to avoid clashing with Thor's option).

WEB_FRAMEWORK_RE = /(?:^|\n)\s*require\s+["'](?:sinatra|rails|action_controller|active_record|grape|hanami|roda|rack|rackup|puma|unicorn|thin)\b|<\s*(?:Sinatra::Base|Grape::API|ApplicationController)\b|Rails\.application/

Web frameworks: their ENV reads are config, not a CLI surface.

Instance Method Summary

Instance methods inherited from class Analyzer::Ruby::RubyEngine

line_to_endpoint(content : String, details : Details | Nil = nil) : Endpoint line_to_endpoint

Class methods inherited from class Analyzer::Ruby::RubyEngine

ruby_test_path?(path : String) : Bool ruby_test_path?

Instance methods inherited from class Analyzer

analyze analyze, base_path : String base_path, base_paths : Array(String) base_paths, callees_needed? : Bool callees_needed?, http_header_name(name : String) : String | Nil http_header_name, line_number_for_index(content : String, char_index : Int32) : Int32 line_number_for_index, logger : NoirLogger logger, parallel_analyze(files : Array(String), &block : String -> Nil) parallel_analyze, read_file_content(path : String) : String read_file_content, result : Array(Endpoint) result, unique_params(params : Array(Param)) : Array(Param) unique_params, url : String url, web_root_path(path : String, markers : Array(String)) : String web_root_path

Constructor methods inherited from class Analyzer

new(options : Hash(String, YAML::Any)) new

Instance methods inherited from module FileHelper

all_files : Array(String) all_files, get_files_by_extension(extension : String) : Array(String) get_files_by_extension, get_files_by_extensions(extensions : Array(String)) : Array(String) get_files_by_extensions, get_files_by_prefix(prefix : String) : Array(String) get_files_by_prefix, get_files_by_prefix_and_extension(prefix : String, extension : String) : Array(String) get_files_by_prefix_and_extension, get_public_dir_files(base_path : String, folder : String) : Array(String) get_public_dir_files, get_public_files(base_path : String, anchors : Array(String) = ["shard.yml", "Gemfile"]) : Array(String) get_public_files

Instance Method Detail

def analyze #

[View source]