Last active
May 30, 2025 14:53
-
-
Save wteuber/9dd0502601f8dd7cbd6add10cf5aa6d2 to your computer and use it in GitHub Desktop.
Ruby namespace checker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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