Files
homebrew-tap/scripts/parse_formulas.rb
Ismo Vuorinen 6dc9a170cc chore: fixes, several improvements and refactorings (#2)
* chore: fixes

* chore: rubocop fixes, linting, etc.

* chore: switching to use `brew style` only

* chore: use `brew style` for linting, skip example formulae in ci.yml

* chore(lint): fixes, additions and tweaks
2025-09-23 11:29:53 +03:00

146 lines
4.0 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# typed: strict
# frozen_string_literal: true
require "json"
require "fileutils"
require "pathname"
require "date"
# Simple polyfill for Homebrew extensions
class String
def blank?
# Polyfill implementation to avoid external dependencies
nil? || empty? # rubocop:disable Homebrew/Blank
end
end
# Parser class for extracting metadata from Homebrew formulae
class FormulaParser
FORMULA_DIR = File.expand_path("../Formula", __dir__).freeze
OUTPUT_DIR = File.expand_path("../docs/_data", __dir__).freeze
OUTPUT_FILE = File.join(OUTPUT_DIR, "formulae.json").freeze
# Regex patterns for safe extraction without code evaluation
PATTERNS = {
class_name: /^class\s+(\w+)\s+<\s+Formula/,
desc: /^\s*desc\s+["']([^"']+)["']/,
homepage: /^\s*homepage\s+["']([^"']+)["']/,
url: /^\s*url\s+["']([^"']+)["']/,
version: /^\s*version\s+["']([^"']+)["']/,
sha256: /^\s*sha256\s+["']([a-f0-9]{64})["']/i,
license: /^\s*license\s+["']([^"']+)["']/,
depends_on: /^\s*depends_on\s+["']([^"']+)["']/,
}.freeze
def self.run
new.generate_documentation_data
end
def generate_documentation_data
ensure_output_directory
formulae = parse_all_formulae
write_json_output(formulae)
puts "✅ Successfully generated documentation for #{formulae.length} formulae"
end
private
def ensure_output_directory
FileUtils.mkdir_p(OUTPUT_DIR)
end
def parse_all_formulae
formula_files.filter_map { |file| parse_formula(file) }.sort_by { |f| f[:name] }
end
def formula_files
Dir.glob(File.join(FORMULA_DIR, "**", "*.rb"))
end
def parse_formula(file_path)
content = File.read(file_path)
class_name = extract_value(content, :class_name)
return unless class_name
formula_name = convert_class_name_to_formula_name(class_name)
return if formula_name.blank?
build_formula_metadata(content, file_path, formula_name, class_name)
rescue => e
warn "⚠️ Error parsing #{file_path}: #{e.message}"
nil
end
def build_formula_metadata(content, file_path, formula_name, class_name)
{
name: formula_name,
class_name: class_name,
description: extract_value(content, :desc),
homepage: extract_value(content, :homepage),
url: extract_value(content, :url),
version: extract_version(content),
sha256: extract_value(content, :sha256),
license: extract_value(content, :license),
dependencies: extract_dependencies(content),
file_path: calculate_relative_path(file_path),
last_modified: format_time_iso8601(File.mtime(file_path)),
}
end
def calculate_relative_path(file_path)
Pathname.new(file_path).relative_path_from(Pathname.new(FORMULA_DIR)).to_s
end
def extract_value(content, pattern_key)
match = content.match(PATTERNS[pattern_key])
match&.[](1)
end
def extract_version(content)
# Try explicit version first, then extract from URL
explicit = extract_value(content, :version)
return explicit if explicit
url = extract_value(content, :url)
return unless url
# Common version patterns in URLs
url.match(/v?(\d+(?:\.\d+)+)/)&.[](1)
end
def extract_dependencies(content)
content.scan(PATTERNS[:depends_on]).flatten.uniq
end
def convert_class_name_to_formula_name(class_name)
return unless class_name
# Convert CamelCase to kebab-case
class_name
.gsub(/([a-z\d])([A-Z])/, "\1-\2")
.downcase
end
def format_time_iso8601(time)
# Format time manually for compatibility
time.strftime("%Y-%m-%dT%H:%M:%S%z").gsub(/(\d{2})(\d{2})$/, "\1:\2")
end
def write_json_output(formulae)
output = {
tap_name: "ivuorinen/tap",
generated_at: format_time_iso8601(Time.now),
formulae_count: formulae.length,
formulae: formulae,
}
File.write(OUTPUT_FILE, JSON.pretty_generate(output))
end
end
# Run if executed directly
FormulaParser.run if __FILE__ == $PROGRAM_NAME