1# frozen_string_literal: true
2
3desc "Verify mixin ivar requirements using Solargraph"
4task :verify_mixin_ivars do
5 require "set"
6 require "solargraph"
7 require_relative "../solargraph_ruby34_fix"
8
9 if defined?(RubyVM::AbstractSyntaxTree)
10 def find_ivars(node, ivars=Set.new)
11 return ivars unless node.is_a?(RubyVM::AbstractSyntaxTree::Node)
12
13 ivars.add(node.children[0].to_s) if [:IASGN, :IVAR].include?(node.type)
14 node.children.filter { |child|
15 child.is_a?(RubyVM::AbstractSyntaxTree::Node)
16 }.each do |child|
17 find_ivars(child, ivars)
18 end
19
20 ivars
21 end
22 else
23 require "parser/current"
24
25 def find_ivars(node, ivars=Set.new)
26 return ivars unless node.is_a?(Parser::AST::Node)
27
28 ivars.add(node.children[0].to_s) if [:ivasgn, :ivar].include?(node.type)
29
30 node.children.each do |child|
31 find_ivars(child, ivars)
32 end
33
34 ivars
35 end
36 end
37
38 lib_files = Dir.glob("lib/**/*.rb")
39 sources = lib_files.map { |file|
40 Solargraph::Source.load_string(File.read(file), file)
41 }
42 # @type [Array<Solargraph::SourceMap>]
43 source_maps = sources.filter_map { |source|
44 Solargraph::SourceMap.map(source)
45 }
46 violations = []
47
48 modules_with_ivars = {}
49
50 all_pins = source_maps.flat_map(&:pins)
51 # @type [Array<Solargraph::Pin::Namespace>]
52 module_pins = all_pins.select { |p|
53 p.is_a?(Solargraph::Pin::Namespace) && p.type == :module
54 }
55
56 module_pins.each do |mod|
57 source_map = source_maps.find { |sm| sm.filename == mod.location.filename }
58 next unless source_map
59
60 ivars = Set.new
61 source_map.pins.each do |pin|
62 next unless pin.is_a?(Solargraph::Pin::Method)
63 next unless pin.closure && pin.closure.path == mod.path
64
65 find_ivars(pin.node, ivars) if pin.node
66 end
67
68 modules_with_ivars[mod] = ivars.to_a unless ivars.empty?
69 end
70
71 # @type [Array<Solargraph::Pin::Namespace>]
72 class_pins = all_pins.select { |p|
73 p.is_a?(Solargraph::Pin::Namespace) && p.type == :class
74 }
75
76 class_pins.each do |klass|
77 source_map = source_maps.find { |sm|
78 sm.filename == klass.location.filename
79 }
80 next unless source_map
81
82 # @type [Array<Solargraph::Pin::Reference::Include>]
83 included_modules = source_map.pins.select { |p|
84 p.is_a?(Solargraph::Pin::Reference::Include) && p.closure == klass
85 }
86
87 next if included_modules.empty?
88
89 init_method = source_map.pins.find { |p|
90 p.is_a?(Solargraph::Pin::Method) &&
91 p.name == "initialize" &&
92 p.closure && p.closure.path == klass.path
93 }
94
95 init_ivars = Set.new
96 if init_method
97 source_map.pins.each do |pin|
98 if pin.is_a?(Solargraph::Pin::InstanceVariable) &&
99 pin.closure == init_method
100 init_ivars.add(pin.name)
101 end
102 end
103 end
104
105 included_modules.each do |include_pin|
106 mod = module_pins.find { |m| m.path == include_pin.name }
107 next unless mod
108
109 required_ivars = modules_with_ivars[mod]
110 next unless required_ivars
111
112 missing = Set.new(required_ivars) - init_ivars
113
114 missing.each do |ivar|
115 violations << {
116 file: klass.location.filename,
117 line: klass.location.range.start.line + 1,
118 class: klass.path,
119 module: mod.path,
120 ivar: ivar
121 }
122 end
123 end
124 end
125
126 if violations.empty?
127 puts "✓ All classes properly initialize ivars required by their mixins"
128 else
129 puts "✗ Found #{violations.size} missing ivar initialization(s):\n\n"
130
131 violations.each do |v|
132 puts "#{v[:file]}:#{v[:line]}"
133 puts " Class '#{v[:class]}' includes module '#{v[:module]}'"
134 puts " which references '#{v[:ivar]}',"
135 puts " but '#{v[:class]}#initialize' doesn't set it"
136 puts " Add '#{v[:ivar]} = ...' to the initialize method"
137 puts
138 end
139
140 exit 1
141 end
142end