Class: Scryer::Scanner
- Inherits:
-
Object
- Object
- Scryer::Scanner
- Defined in:
- lib/scryer/scanner.rb
Overview
Ties parsing + rules + duplicate detection together across a directory
tree. This is the one entry point both the sentinel:scan rake task and
any generator/CLI should call — everything else in this gem is a building
block this class composes.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFAULT_GLOB_DIRS =
%w[app lib config db].freeze
- SKIP_DIR_SEGMENTS =
%w[vendor node_modules tmp log .git spec test].freeze
- DUPLICATE_DETECTION_APP_SUBDIRS =
Duplicate-code detection only makes sense within hand-written business logic. Migrations in particular are mostly boilerplate (create_table / t.string / t.integer calls) that normalize to near-identical token streams and drown out real duplicates with false positives — so unlike the security/performance rules (which still scan every file under DEFAULT_GLOB_DIRS), duplicate detection is restricted to models, controllers, helpers, and concerns (wherever they're nested).
%w[models controllers helpers].freeze
- QUERY_SIMILARITY_THRESHOLD =
Query and cache-value duplicates use a slightly higher bar than whole methods: they're much shorter fragments, so a coincidental match at the method threshold (0.6) is more likely — see DuplicateDetector's shingle comment for why short token streams are more sensitive to this.
0.7- CACHE_SIMILARITY_THRESHOLD =
0.7
Instance Method Summary collapse
- #call ⇒ Object
-
#initialize(root:, dirs: DEFAULT_GLOB_DIRS, skip_rules: []) ⇒ Scanner
constructor
skip_rulessilences specific checks by rule_id (e.g. a known false positive on this codebase) without editing/removing the rule itself — accepts strings or symbols, matched against Rule.rule_id.
Constructor Details
#initialize(root:, dirs: DEFAULT_GLOB_DIRS, skip_rules: []) ⇒ Scanner
skip_rules silences specific checks by rule_id (e.g. a known false
positive on this codebase) without editing/removing the rule itself —
accepts strings or symbols, matched against Rule.rule_id.
34 35 36 37 38 |
# File 'lib/scryer/scanner.rb', line 34 def initialize(root:, dirs: DEFAULT_GLOB_DIRS, skip_rules: []) @root = File.(root) @dirs = dirs @skip_rules = Set.new(skip_rules.map(&:to_s)) end |
Instance Method Details
#call ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 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 |
# File 'lib/scryer/scanner.rb', line 40 def call files = collect_files all_methods = [] all_queries = [] all_cache_calls = [] security_findings = [] performance_findings = [] style_findings = [] parse_errors = [] files.each do |abs_path| rel_path = abs_path.sub(/\A#{Regexp.escape(@root)}\/?/, "") source = File.read(abs_path) sexp = begin Ripper.sexp(source) rescue StandardError => e parse_errors << { file: rel_path, error: e. } nil end if sexp.nil? parse_errors << { file: rel_path, error: "Ripper could not parse this file (possibly a syntax error, or Ruby syntax newer than this gem's Ruby runtime supports)" } unless parse_errors.any? { |pe| pe[:file] == rel_path } next end RuleSet.all.each do |rule_class| next if @skip_rules.include?(rule_class.rule_id.to_s) bucket = case rule_class.category when "security" then security_findings when "performance" then performance_findings when "style" then style_findings end next unless bucket bucket.concat(rule_class.new(file: rel_path, source: source, sexp: sexp).scan) end if duplicate_detection_target?(rel_path) all_methods.concat(MethodExtractor.extract(file: rel_path, source: source, sexp: sexp)) all_queries.concat(QueryExtractor.extract(file: rel_path, source: source, sexp: sexp)) all_cache_calls.concat(CacheExtractor.extract(file: rel_path, source: source, sexp: sexp)) end end # Same computed value cached under the same key from multiple call # sites is normal (just reusing the cache). Only flag it when the # *keys* differ too — that's either a redundant cache entry or a key # that drifted out of sync with a copy-pasted sibling. cache_groups = DuplicateDetector.call(all_cache_calls, threshold: CACHE_SIMILARITY_THRESHOLD, kind: "cache_duplicate") .select { |g| g.members.map(&:cache_key).uniq.size > 1 } duplicate_groups = DuplicateDetector.call(all_methods, kind: "method_duplicate") + DuplicateDetector.call(all_queries, threshold: QUERY_SIMILARITY_THRESHOLD, kind: "query_duplicate") + cache_groups Result.new( security_findings: security_findings, performance_findings: performance_findings, style_findings: style_findings, duplicate_groups: duplicate_groups, files_scanned: files.size, parse_errors: parse_errors ) end |