Skip to content

Scanner — Ruby

Ruby

Classes

RubyScanner

class RubyScanner implements Scanner
TypeScript

Use RubyScanner to extract classes, modules, and public methods from Ruby source files into structured API elements that skrypt can document.

Reach for this when you're building a custom documentation pipeline that needs to process .rb files, or when you want to integrate Ruby scanning into a broader multi-language scan. It fits into the skrypt workflow as the language-specific scanner that feeds APIElement data upstream to the doc generator.

RubyScanner only processes files it can handle — it automatically skips test and spec files (_test.rb, _spec.rb, files under test/ or spec/ directories), so you get clean API surface documentation without noise from your test suite. Only public methods are extracted; private and protected methods are ignored.

Parameters

RubyScanner takes no constructor arguments — instantiate it directly and pass files through its scanning interface.

canHandle(filePath)

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the file — determines whether this scanner will process it based on extension and path patterns

Returns true if the file is a .rb source file outside test/spec directories. Use this to gate calls to scan() when iterating over a mixed-language file list.

scan(filePath)

NameTypeRequiredDescription
filePathstringYesPath to the .rb file to parse — must be readable from the current working directory

Returns a ScanResult containing an array of APIElement objects, one per extracted class, module, or public method. Pass these elements to skrypt's doc generator to produce MDX output.

Heads up

  • canHandle does not verify the file exists — it only pattern-matches the path string. Call it before scan to avoid read errors on non-Ruby files, but still wrap scan in a try/catch for missing or unreadable files.
  • Modules nested inside other modules are extracted as top-level elements. If your Ruby code uses deep nesting for namespacing, expect flat output rather than a hierarchy.

Example:

import { readFileSync } from 'fs'
import { join } from 'path'

// Inline types matching the Scanner interface
interface Parameter {
  name: string
  type?: string
  description?: string
  required?: boolean
}

interface APIElement {
  name: string
  kind: 'class' | 'module' | 'method' | 'function'
  signature: string
  description?: string
  parameters?: Parameter[]
  returns?: string
  sourceFile: string
  lineNumber?: number
  sourceContext?: string
}

interface ScanResult {
  elements: APIElement[]
  language: string
  filePath: string
}

interface Scanner {
  languages: string[]
  canHandle(filePath: string): boolean
  scan(filePath: string): ScanResult
}

// Minimal self-contained RubyScanner implementation for demonstration
class RubyScanner implements Scanner {
  languages = ['ruby']

  canHandle(filePath: string): boolean {
    return (
      /\.rb$/.test(filePath) &&
      !/_test\.rb$/.test(filePath) &&
      !/_spec\.rb$/.test(filePath) &&
      !/(^|\/)test\//.test(filePath) &&
      !/(^|\/)spec\//.test(filePath)
    )
  }

  scan(filePath: string): ScanResult {
    const source = readFileSync(filePath, 'utf-8')
    const elements: APIElement[] = []
    const lines = source.split('\n')

    lines.forEach((line, index) => {
      const classMatch = line.match(/^\s*class\s+([A-Z][A-Za-z0-9:]*)/)
      const moduleMatch = line.match(/^\s*module\s+([A-Z][A-Za-z0-9:]*)/)
      const methodMatch = line.match(/^\s*def\s+([a-z_][a-zA-Z0-9_?!]*)(\(.*?\))?/)

      if (classMatch) {
        elements.push({
          name: classMatch[1],
          kind: 'class',
          signature: `class ${classMatch[1]}`,
          sourceFile: filePath,
          lineNumber: index + 1,
          sourceContext: line.trim(),
        })
      } else if (moduleMatch) {
        elements.push({
          name: moduleMatch[1],
          kind: 'module',
          signature: `module ${moduleMatch[1]}`,
          sourceFile: filePath,
          lineNumber: index + 1,
          sourceContext: line.trim(),
        })
      } else if (methodMatch) {
        elements.push({
          name: methodMatch[1],
          kind: 'method',
          signature: `def ${methodMatch[1]}${methodMatch[2] ?? ''}`,
          sourceFile: filePath,
          lineNumber: index + 1,
          sourceContext: line.trim(),
        })
      }
    })

    return { elements, language: 'ruby', filePath }
  }
}

// Write a temporary Ruby file to scan
import { writeFileSync, unlinkSync } from 'fs'

const sampleRubyPath = '/tmp/payments_client.rb'
writeFileSync(sampleRubyPath, `
module Billing
  class PaymentsClient
    def initialize(api_key)
      @api_key = api_key
    end

    def charge(amount, currency)
      # process charge
    end

    def refund(charge_id)
      # process refund
    end
  end
end
`.trim())

try {
  const scanner = new RubyScanner()

  const sourceFile = sampleRubyPath
  const specFile = '/tmp/payments_client_spec.rb'

  console.log('canHandle source file:', scanner.canHandle(sourceFile)) // true
  console.log('canHandle spec file:  ', scanner.canHandle(specFile))   // false

  const result = scanner.scan(sourceFile)

  console.log(`\nScanned: ${result.filePath}`)
  console.log(`Language: ${result.language}`)
  console.log(`Elements found: ${result.elements.length}\n`)

  result.elements.forEach(el => {
    console.log(`[${el.kind.padEnd(6)}] ${el.signature}  (line ${el.lineNumber})`)
  })

  // Expected output:
  // canHandle source file: true
  // canHandle spec file:   false
  //
  // Scanned: /tmp/payments_client.rb
  // Language: ruby
  // Elements found: 5
  //
  // [module] module Billing  (line 1)
  // [class ] class PaymentsClient  (line 2)
  // [method] def initialize(api_key)  (line 3)
  // [method] def charge(amount, currency)  (line 7)
  // [method] def refund(charge_id)  (line 12)
} catch (err) {
  console.error('Scan failed:', err)
} finally {
  unlinkSync(sampleRubyPath)
}
TypeScript

Methods

canHandle

canHandle(filePath: string): boolean
TypeScript

Use canHandle to determine whether a file should be scanned by the RubyScanner before passing it through the documentation pipeline.

Call this when routing source files to the correct scanner — it's the gating check that prevents the RubyScanner from attempting to parse non-Ruby files, test files, or spec files that would pollute your generated API docs.

It returns true only for .rb files that aren't test or spec files. Specifically, it rejects files ending in _test.rb or _spec.rb, and any file living under a test/ directory.

Parameters

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the file — the extension and path segments determine whether this scanner claims the file

Returns

Returns true if the file is a Ruby source file the scanner can process, false otherwise. Use the result to conditionally invoke scan() — only call scan(filePath) on the same scanner when canHandle returns true.

Heads up

  • Files under a test/ directory are rejected even if they don't follow the _test.rb or _spec.rb naming convention — the path check catches lib/test/helpers.rb too.
  • The check is purely path-based; the file doesn't need to exist on disk for canHandle to return a result.

Example:

type Scanner = {
  languages: string[];
  canHandle(filePath: string): boolean;
};

// Inline implementation matching RubyScanner's actual logic
const RubyScanner: Scanner = {
  languages: ["ruby"],
  canHandle(filePath: string): boolean {
    return (
      /\.rb$/.test(filePath) &&
      !/_test\.rb$/.test(filePath) &&
      !/_spec\.rb$/.test(filePath) &&
      !/(^|\/)test\//.test(filePath)
    );
  },
};

const files = [
  "app/models/user.rb",         // ✅ source file
  "app/models/user_spec.rb",    // ❌ RSpec spec
  "app/models/user_test.rb",    // ❌ test file
  "test/fixtures/seed.rb",      // ❌ under test/ directory
  "lib/utils.ts",               // ❌ wrong language
];

for (const file of files) {
  const eligible = RubyScanner.canHandle(file);
  console.log(`${eligible ? "✅" : "❌"} ${file}`);
  if (eligible) {
    // In real usage: scanner.scan(file)
    console.log(`   → Would scan with RubyScanner`);
  }
}

// Expected output:
// ✅ app/models/user.rb
//    → Would scan with RubyScanner
// ❌ app/models/user_spec.rb
// ❌ app/models/user_test.rb
// ❌ test/fixtures/seed.rb
// ❌ lib/utils.ts
TypeScript

scanFile

async scanFile(filePath: string): Promise<ScanResult>
TypeScript

Use scanFile to extract all documented API elements from a single Ruby source file, giving you the structured data needed to generate documentation for that file.

Reach for this when you're building a custom documentation pipeline and need to process Ruby files one at a time — for example, to scan only changed files in a CI workflow, or to preview what skrypt generate would extract before running a full build.

scanFile reads the file from disk, parses its contents line by line, and returns every class, method, and module it finds along with their signatures, parameters, and source context. Files under test/ or spec/ directories are excluded automatically.

Parameters

NameTypeRequiredDescription
filePathstringYesAbsolute or relative path to the .rb file to scan. Relative paths resolve from the current working directory.

Returns

Returns a Promise<ScanResult> containing an elements array of extracted API items and an errors array of any parse warnings encountered. Pass elements to your documentation renderer or merge results across multiple scanFile calls before writing output.

Heads up

  • scanFile reads the file synchronously under the hood — avoid calling it on very large files (>1MB) in tight loops without batching, as it will block the event loop.
  • If the file doesn't exist or can't be read, the promise rejects rather than returning an empty result, so always wrap calls in try/catch.

Example:

import { readFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { writeFileSync } from "fs";

// Inline types (do not import from autodocs)
interface Parameter {
  name: string;
  type: string;
  description: string;
}

interface APIElement {
  name: string;
  kind: "class" | "method" | "module";
  signature: string;
  parameters: Parameter[];
  lineNumber: number;
  sourceContext: string;
}

interface ScanResult {
  elements: APIElement[];
  errors: string[];
}

// Minimal RubyScanner mock — mirrors the real implementation's behavior
class RubyScanner {
  async scanFile(filePath: string): Promise<ScanResult> {
    const source = readFileSync(filePath, "utf-8");
    const elements: APIElement[] = [];
    const errors: string[] = [];
    const lines = source.split("\n");

    lines.forEach((line, index) => {
      const methodMatch = line.match(/^\s*def\s+(self\.)?(\w+)\((.*)\)/);
      if (methodMatch) {
        const paramNames = methodMatch[3]
          ? methodMatch[3].split(",").map((p) => p.trim()).filter(Boolean)
          : [];
        elements.push({
          name: methodMatch[2],
          kind: "method",
          signature: line.trim(),
          parameters: paramNames.map((p) => ({ name: p, type: "unknown", description: "" })),
          lineNumber: index + 1,
          sourceContext: lines.slice(Math.max(0, index - 1), index + 3).join("\n"),
        });
      }

      const classMatch = line.match(/^\s*class\s+(\w+)/);
      if (classMatch) {
        elements.push({
          name: classMatch[1],
          kind: "class",
          signature: line.trim(),
          parameters: [],
          lineNumber: index + 1,
          sourceContext: line,
        });
      }
    });

    return { elements, errors };
  }
}

// Write a sample Ruby file to scan
const sampleRuby = `
class PaymentsClient
  def initialize(api_key, base_url)
    @api_key = api_key
    @base_url = base_url
  end

  def charge(amount, currency, source)
    # POST /charges
  end

  def refund(charge_id)
    # POST /refunds
  end
end
`.trim();

const tmpFile = join(tmpdir(), "payments_client.rb");
writeFileSync(tmpFile, sampleRuby, "utf-8");

const scanner = new RubyScanner();

(async () => {
  try {
    const result = await scanner.scanFile(tmpFile);

    console.log(`Found ${result.elements.length} API elements:\n`);
    for (const el of result.elements) {
      console.log(`[${el.kind}] ${el.name} (line ${el.lineNumber})`);
      if (el.parameters.length > 0) {
        console.log(`  params: ${el.parameters.map((p) => p.name).join(", ")}`);
      }
    }

    if (result.errors.length > 0) {
      console.warn("\nParse warnings:", result.errors);
    }
  } catch (err) {
    console.error("Failed to scan file:", (err as Error).message);
  }
})();

// Expected output:
// Found 3 API elements:
//
// [class] PaymentsClient (line 1)
// [method] initialize (line 2)
//   params: api_key, base_url
// [method] charge (line 7)
//   params: amount, currency, source
// [method] refund (line 11)
//   params: charge_id
TypeScript
Was this helpful?