Skip to content

Instantly share code, notes, and snippets.

@wteuber
Last active May 30, 2025 14:53
Show Gist options
  • Save wteuber/9dd0502601f8dd7cbd6add10cf5aa6d2 to your computer and use it in GitHub Desktop.
Save wteuber/9dd0502601f8dd7cbd6add10cf5aa6d2 to your computer and use it in GitHub Desktop.
Ruby namespace checker
#!/usr/bin/env ruby
# frozen_string_literal: true
# Example usage:
# ./check_namespaces.rb
# for dir in */; do (cd "$dir" && ../check_namespaces.rb); done
# ./check_namespaces.rb path/to/test/
# ls -d *components/**/test/* | xargs ./check_namespaces.rb
require "prism"
require "pathname"
require "fileutils"
require "set"
require "parallel"
module NamespaceChecker
IGNORED_DIRS = [".", ".."].freeze
FILE_EXTENSION = ".rb".freeze
class Logger
def self.log(message, type: :info)
case type
when :success
#puts "✅ #{message}"
when :error
puts "❌ #{message}"
when :warning
puts "⚠️ #{message}"
when :info
puts message
end
end
end
class Checker
def self.run(pwd, relative_dir)
new(pwd, relative_dir).run
end
def initialize(pwd, relative_dir)
@pwd = pwd
@namespace_root = File.join(pwd, relative_dir)
end
def run
files = Dir.glob("#{@namespace_root}/**/*#{FILE_EXTENSION}")
Parallel.each(files, in_processes: Parallel.processor_count) do |file|
check_compatibility(file)
rescue StandardError => e
Logger.log("Error processing #{file}: #{e.message}", type: :error)
end
end
private
def check_compatibility(absolute_path)
return unless File.exist?(absolute_path) && File.readable?(absolute_path)
path_from_pwd = absolute_path.delete_prefix("#{@pwd}/")
relative_path = absolute_path.delete_prefix("#{@namespace_root}/")
expected_namespace = build_expected_namespace(relative_path)
found_namespaces = namespaces(absolute_path)
if found_namespaces.empty?
Logger.log("#{path_from_pwd} (No namespaces found)", type: :warning)
elsif found_namespaces.include?(expected_namespace)
Logger.log(path_from_pwd, type: :success)
else
Logger.log("#{path_from_pwd}\n Expected: #{expected_namespace}\n Found: #{found_namespaces.join(", ")}", type: :error)
end
end
def build_expected_namespace(relative_path)
@namespace_cache ||= {}
return @namespace_cache[relative_path] if @namespace_cache.key?(relative_path)
path_parts = Pathname(relative_path).dirname.to_s.split(File::SEPARATOR)
.reject { |part| IGNORED_DIRS.include?(part) }
.reject(&:empty?)
expected_modules = path_parts.map { |part| part.split("_").map(&:capitalize).join }
expected_class = File.basename(relative_path, FILE_EXTENSION)
.split("_")
.map(&:capitalize)
.join
@namespace_cache[relative_path] = (expected_modules + [expected_class]).join("::")
end
def namespaces(absolute_path)
content = File.read(absolute_path)
ast = Prism.parse(content)
return Set.new unless ast.success?
namespaces = Set.new
find_namespaces = lambda do |node, current_namespace = []|
return unless node.is_a?(Prism::Node)
case node
when Prism::ModuleNode, Prism::ClassNode
name = extract_const_name(node.constant_path)
if name && !name.empty?
current_namespace.push(name)
namespaces << current_namespace.join("::")
end
end
node.child_nodes&.each { |child| find_namespaces.call(child, current_namespace.dup) }
end
find_namespaces.call(ast.value)
namespaces
rescue Prism::ParseError => e
puts "Failed to parse #{absolute_path}: #{e.message}"
Set.new
end
def extract_const_name(node)
return unless node.is_a?(Prism::Node)
case node
when Prism::ConstantPathNode
parts = []
current = node
while current.is_a?(Prism::ConstantPathNode)
name = current.name.to_s
return nil if name.empty?
parts.unshift(name)
current = current.parent
end
parts.join("::")
when Prism::ConstantReadNode
name = node.name.to_s
name.empty? ? nil : name
end
end
end
end
if __FILE__ == $PROGRAM_NAME
pwd = FileUtils.pwd
if ARGV.empty?
NamespaceChecker::Logger.log("No directories specified. Checking current directory.", type: :info)
NamespaceChecker::Checker.run(pwd, ".")
else
ARGV.each do |relative_dir|
unless Dir.exist?(File.join(pwd, relative_dir))
NamespaceChecker::Logger.log("Directory not found: #{relative_dir}", type: :error)
next
end
NamespaceChecker::Checker.run(pwd, relative_dir)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment