Module: Envdoctor::Scanner
- Defined in:
- lib/envdoctor/scanner.rb
Overview
Core scanner: reconcile ENV usage in Ruby source against .env definitions. Local-first — no network, values never printed.
Defined Under Namespace
Constant Summary collapse
- USAGE_PATTERNS =
[ /\bENV\[\s*["']([A-Za-z_]\w*)["']\s*\]/, /\bENV\.fetch\(\s*["']([A-Za-z_]\w*)["']/ ].freeze
- ENV_LINE =
/\A\s*(?:export\s+)?([A-Za-z_]\w*)\s*=/.freeze
Class Method Summary collapse
- .discover_env_files(root) ⇒ Object
- .discover_source_files(root) ⇒ Object
- .parse_env(path, content) ⇒ Object
- .relative(root, path) ⇒ Object
- .scan(root) ⇒ Object
- .scan_source(path, content) ⇒ Object
-
.strip_noise(code) ⇒ Object
Blank comments and =begin/=end blocks, preserving line structure.
Class Method Details
.discover_env_files(root) ⇒ Object
54 55 56 57 58 |
# File 'lib/envdoctor/scanner.rb', line 54 def discover_env_files(root) files = Dir.glob(File.join(root, ".env")) files += Dir.glob(File.join(root, ".env.*")).reject { |f| f.end_with?(".example") } files.sort end |
.discover_source_files(root) ⇒ Object
60 61 62 63 64 |
# File 'lib/envdoctor/scanner.rb', line 60 def discover_source_files(root) Dir.glob(File.join(root, "**", "*.rb")).reject do |p| p.split(File::SEPARATOR).any? { |part| %w[.git vendor node_modules].include?(part) } end.sort end |
.parse_env(path, content) ⇒ Object
41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/envdoctor/scanner.rb', line 41 def parse_env(path, content) defined = {} content.split("\n").each_with_index do |raw, i| stripped = raw.strip next if stripped.empty? || stripped.start_with?("#") if (m = raw.match(ENV_LINE)) defined[m[1]] ||= Origin.new(path, i + 1) end end defined end |
.relative(root, path) ⇒ Object
94 95 96 |
# File 'lib/envdoctor/scanner.rb', line 94 def relative(root, path) path.sub(/\A#{Regexp.escape(root)}#{Regexp.escape(File::SEPARATOR)}?/, "") end |
.scan(root) ⇒ Object
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/envdoctor/scanner.rb', line 66 def scan(root) defined = {} discover_env_files(root).each do |f| parse_env(relative(root, f), File.read(f)).each { |k, v| defined[k] ||= v } end used = {} discover_source_files(root).each do |f| scan_source(relative(root, f), File.read(f)).each { |k, v| used[k] ||= v } end findings = [] used.keys.sort.each do |name| next if defined.key?(name) findings << Finding.new("undefined-in-source", "error", name, "used in source code but not defined in any environment file", used[name]) end defined.keys.sort.each do |name| next if used.key?(name) findings << Finding.new("unused", "warning", name, "defined but never referenced in source", defined[name]) end findings end |
.scan_source(path, content) ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/envdoctor/scanner.rb', line 25 def scan_source(path, content) text = strip_noise(content) used = {} USAGE_PATTERNS.each do |re| text.to_enum(:scan, re).each do match = Regexp.last_match name = match[1] next if used.key?(name) line = text[0...match.begin(0)].count("\n") + 1 used[name] = Origin.new(path, line) end end used end |
.strip_noise(code) ⇒ Object
Blank comments and =begin/=end blocks, preserving line structure.
20 21 22 23 |
# File 'lib/envdoctor/scanner.rb', line 20 def strip_noise(code) code = code.gsub(/^=begin\b.*?^=end\b[^\n]*/m) { |m| m.gsub(/[^\n]/, " ") } code.gsub(/#[^\n]*/) { |m| " " * m.length } end |