# frozen_string_literal: true desc "Verify mixin ivar requirements using Solargraph" task :verify_mixin_ivars do require "set" require "solargraph" require_relative "../solargraph_ruby34_fix" if defined?(RubyVM::AbstractSyntaxTree) def find_ivars(node, ivars=Set.new) return ivars unless node.is_a?(RubyVM::AbstractSyntaxTree::Node) ivars.add(node.children[0].to_s) if [:IASGN, :IVAR].include?(node.type) node.children.filter { |child| child.is_a?(RubyVM::AbstractSyntaxTree::Node) }.each do |child| find_ivars(child, ivars) end ivars end else require "parser/current" def find_ivars(node, ivars=Set.new) return ivars unless node.is_a?(Parser::AST::Node) ivars.add(node.children[0].to_s) if [:ivasgn, :ivar].include?(node.type) node.children.each do |child| find_ivars(child, ivars) end ivars end end lib_files = Dir.glob("lib/**/*.rb") sources = lib_files.map { |file| Solargraph::Source.load_string(File.read(file), file) } # @type [Array] source_maps = sources.filter_map { |source| Solargraph::SourceMap.map(source) } violations = [] modules_with_ivars = {} all_pins = source_maps.flat_map(&:pins) # @type [Array] module_pins = all_pins.select { |p| p.is_a?(Solargraph::Pin::Namespace) && p.type == :module } module_pins.each do |mod| source_map = source_maps.find { |sm| sm.filename == mod.location.filename } next unless source_map ivars = Set.new source_map.pins.each do |pin| next unless pin.is_a?(Solargraph::Pin::Method) next unless pin.closure && pin.closure.path == mod.path find_ivars(pin.node, ivars) if pin.node end modules_with_ivars[mod] = ivars.to_a unless ivars.empty? end # @type [Array] class_pins = all_pins.select { |p| p.is_a?(Solargraph::Pin::Namespace) && p.type == :class } class_pins.each do |klass| source_map = source_maps.find { |sm| sm.filename == klass.location.filename } next unless source_map # @type [Array] included_modules = source_map.pins.select { |p| p.is_a?(Solargraph::Pin::Reference::Include) && p.closure == klass } next if included_modules.empty? init_method = source_map.pins.find { |p| p.is_a?(Solargraph::Pin::Method) && p.name == "initialize" && p.closure && p.closure.path == klass.path } init_ivars = Set.new if init_method source_map.pins.each do |pin| if pin.is_a?(Solargraph::Pin::InstanceVariable) && pin.closure == init_method init_ivars.add(pin.name) end end end included_modules.each do |include_pin| mod = module_pins.find { |m| m.path == include_pin.name } next unless mod required_ivars = modules_with_ivars[mod] next unless required_ivars missing = Set.new(required_ivars) - init_ivars missing.each do |ivar| violations << { file: klass.location.filename, line: klass.location.range.start.line + 1, class: klass.path, module: mod.path, ivar: ivar } end end end if violations.empty? puts "✓ All classes properly initialize ivars required by their mixins" else puts "✗ Found #{violations.size} missing ivar initialization(s):\n\n" violations.each do |v| puts "#{v[:file]}:#{v[:line]}" puts " Class '#{v[:class]}' includes module '#{v[:module]}'" puts " which references '#{v[:ivar]}'," puts " but '#{v[:class]}#initialize' doesn't set it" puts " Add '#{v[:ivar]} = ...' to the initialize method" puts end exit 1 end end