Class: ReactManifest::Scanner
- Inherits:
-
Object
- Object
- ReactManifest::Scanner
- Defined in:
- lib/react_manifest/scanner.rb
Overview
Scans JS/JSX (and optionally TS/TSX) files using regex — no AST, no Node.js required.
Returns a Result containing:
-
symbol_index— map of exported symbol name → shared require path -
controller_usages— map of controller name → sorted array of referenced shared files -
warnings— non-fatal issues found during scanning
Phase 1 — builds a symbol index from shared dirs:
"PrimaryButton" => "ux/components/buttons/primary_button"
"useFetch" => "ux/hooks/use_fetch"
Phase 2 — scans controller files for usage of those symbols
and produces per-controller lists of referenced shared files.
Phase 3 — emits non-fatal warnings. rubocop:disable Metrics/ClassLength
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFINITION_PATTERNS =
SymbolExtractor::DEFINITION_PATTERNS
- PASCAL_TOKEN_PATTERN =
SymbolExtractor::PASCAL_TOKEN_PATTERN
- HOOK_TOKEN_PATTERN =
SymbolExtractor::HOOK_TOKEN_PATTERN
- LIB_CALL_PATTERN =
SymbolExtractor::LIB_CALL_PATTERN
- JS_BUILTINS =
SymbolExtractor::JS_BUILTINS
Constants included from PathUtils
PathUtils::STRIPPABLE_EXTENSIONS
Class Method Summary collapse
Instance Method Summary collapse
-
#initialize(config = ReactManifest.configuration) ⇒ Scanner
constructor
A new instance of Scanner.
-
#scan(classification) ⇒ Object
rubocop:disable Metrics/MethodLength,Metrics/AbcSize.
Methods included from Logging
#log_debug, #log_info, #log_warn
Methods included from PathUtils
Constructor Details
#initialize(config = ReactManifest.configuration) ⇒ Scanner
Returns a new instance of Scanner.
47 48 49 |
# File 'lib/react_manifest/scanner.rb', line 47 def initialize(config = ReactManifest.configuration) @config = config end |
Class Method Details
.clear_cache! ⇒ Object
38 39 40 |
# File 'lib/react_manifest/scanner.rb', line 38 def clear_cache! @file_symbol_cache = {} end |
.file_symbol_cache ⇒ Object
34 35 36 |
# File 'lib/react_manifest/scanner.rb', line 34 def file_symbol_cache @file_symbol_cache ||= {} end |
.invalidate(file_path) ⇒ Object
42 43 44 |
# File 'lib/react_manifest/scanner.rb', line 42 def invalidate(file_path) file_symbol_cache.delete(file_path) end |
Instance Method Details
#scan(classification) ⇒ Object
rubocop:disable Metrics/MethodLength,Metrics/AbcSize
52 53 54 55 56 57 58 59 60 61 62 63 64 65 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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/react_manifest/scanner.rb', line 52 def scan(classification) warnings = Set.new symbol_index = {} external_file_paths = {} # file_path => relative_require_path for external_roots files # Phase 1a: index symbols from shared dirs; cache content for violation detection shared_file_paths = {} # file_path => relative_require_path shared_file_content = {} # file_path => raw content string classification.shared_dirs.each do |shared_dir| js_files_in(shared_dir[:path]).each do |file_path| relative = relative_require_path(file_path) shared_file_paths[file_path] = relative content = begin File.read(file_path, encoding: "utf-8") rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError nil end shared_file_content[file_path] = content symbols = extract_definitions_from(file_path, content) symbols.each do |sym| if symbol_index.key?(sym) warnings.add("Duplicate symbol '#{sym}' in #{relative} (already from #{symbol_index[sym]})") else symbol_index[sym] = relative end end end end # Phase 1b: index symbols from external_roots dirs @config.external_roots.each do |root_path| abs_root = abs_external_root(root_path) js_files_in(abs_root).each do |file_path| relative = relative_require_path(file_path) external_file_paths[file_path] = relative symbols = extract_definitions(file_path) symbols.each do |sym| symbol_index[sym] ||= relative end end end # Phase 1c: add explicit external_providers (highest precedence — wins on conflict) @config.external_providers.each do |sym, require_path| symbol_index[sym] = require_path end log_debug "Shared symbol index: #{symbol_index.size} symbols indexed" if @config.verbose? # Phase 2: single pass over controller dirs — build violation index AND detect usages controller_symbol_index = {} controller_usages = {} classification.controller_dirs.each do |ctrl| files = js_files_in(ctrl[:path]) used = Set.new warnings.add("Controller dir '#{ctrl[:name]}' has no JS/JSX files") if files.empty? && @config.verbose? files.each do |file_path| content = read_controller_file(file_path, warnings) next unless content extract_definitions_from(file_path, content).each do |sym| controller_symbol_index[sym] ||= { file: relative_require_path(file_path), controller: ctrl[:name] } end used.merge(extract_used_shared_paths(content, symbol_index)) end controller_usages[ctrl[:name]] = used.to_a.sort end # Phase 3a: detect shared/external files that use app-dir (controller) symbols shared_violations = detect_shared_violations(shared_file_paths, shared_file_content, controller_symbol_index, warnings) external_violations = detect_external_root_violations(external_file_paths, controller_symbol_index, warnings) # Phase 3b: additional warnings emit_fanout_warnings(controller_usages, warnings) Result.new( symbol_index: symbol_index, controller_usages: controller_usages, warnings: warnings.to_a, shared_violations: shared_violations, external_violations: external_violations ) end |