Class: MaquinaComponents::Doctor
- Inherits:
-
Object
- Object
- MaquinaComponents::Doctor
- Defined in:
- lib/maquina_components/doctor.rb
Overview
Migration scanner behind rake maquina:doctor.
Run inside a consuming app, it looks for the app's own CSS/view/JS patterns that a maquina release makes redundant or outright breaks, and prints file:line plus a suggested replacement for each. Advisory only: it never edits anything and never fails a build.
Findings carry the release that introduced them, so the report stays useful across upgrades rather than describing a single migration.
Plain Ruby on purpose - no Rails, no extra gems - so it can also be run against a directory from the engine's own repo.
Defined Under Namespace
Classes: Finding
Constant Summary collapse
- SEVERITIES =
{ breaking: "BREAKING - stops working after upgrading", review: "REVIEW - still works, but there is now a token for it", cleanup: "CLEANUP - probably unnecessary now" }.freeze
- CSS_GLOBS =
[ "app/assets/stylesheets/**/*.css", "app/assets/tailwind/**/*.css", "app/assets/**/*.css", "app/javascript/**/*.css", "app/views/**/*.css" ].freeze
- VIEW_GLOBS =
[ "app/views/**/*.erb", "app/views/**/*.html", "app/components/**/*.erb", "app/helpers/**/*.rb" ].freeze
- JS_GLOBS =
[ "app/javascript/**/*.js" ].freeze
- EXCLUDED =
%r{/(node_modules|tmp|vendor|coverage)/|/app/assets/builds/}- COMPONENT_SELECTOR =
A component-owned selector: data-component or any data-*-part hook.
/\[data-(?:component|[a-z]+(?:-[a-z]+)*-part)\s*[~|^$*]?=/- UNIVERSAL_SELECTOR =
A rule whose every selector is the universal one, e.g.
*or*, ::before, ::after— the shape of a Tailwind-v3-style preflight shim. /\A\*(\s*,\s*(\*|::?[a-z-]+))*\z/- PRESENCE_ACTIVE =
/\[data-active\](?!\s*[~|^$*]?=)/- TAILWIND_ACTIVE_VARIANT =
/data-\[active\]/- SVG_DATA_URI =
/data:image\/svg\+xml/- MARK_TOKEN_ASSIGNMENT =
Assigning a data URI to one of the five mark properties IS the 0.6.0 pattern, so it must not be reported as a restated rule.
/--(?:checkbox-mark|checkbox-indeterminate|radio-mark|switch-thumb|select-chevron)-image\s*:/- RADIUS_DECL =
/(?:border-radius\s*:|@apply[^;{}]*\brounded(?:-[a-z0-9\[\].\/-]+)?\b)/- SHADOW_DECL =
/(?:box-shadow\s*:|@apply[^;{}]*\b(?:shadow|ring)(?:-[a-z0-9\[\].\/-]+)?\b)/- FOCUS_SELECTOR =
/:focus(-visible|-within)?\b/- DESTRUCTIVE_DECL =
0.7.1 ----------------------------------------------------------------- The destructive pair plus the surface it lands on, read out of a host theme. Lightness alone cannot separate the two shipped palette conventions -- a tint palette's dark block has the same shape as a shadcn palette's light block -- so the check measures the symptom (error text disappearing into its own card) instead. See docs/theming.md.
/\A--(destructive|destructive-foreground|card|background)\s*:\s*(.+)\z/m- OKLCH_LIGHTNESS =
/oklch\(\s*([0-9.]+)(%?)/- CONTRAST_FLOOR =
Oklch lightness gap below which text is effectively invisible on its surface. A proxy for a contrast ratio, deliberately conservative: this rule is BREAKING, so it must not fire on a merely low-contrast palette.
0.25- THEME_SCOPE =
Only :root / .dark / @theme declare a palette; a component-level override of the same name is not a statement about the app's convention.
/\A(:root|html|:where\(:root\)|\.dark|html\.dark|\[data-theme[^\]]*\]|)\s*\z/- ERROR_PART =
/data-form-part=["']error["']|form_part:\s*[:"']error/- FIELD_WITH_ERRORS =
/field_with_errors/- ARIA_INVALID =
/aria-invalid|aria:\s*\{[^}]*\binvalid\b|\baria_invalid\b/- HANDROLLED_ERROR_COLOR =
/\btext-destructive\b|\btext-red-\d/- FIELD_OPENER =
A form field, raw or through a Rails helper. Matched against a bounded window rather than a line: helper calls routinely span five or six lines, and a per-line regex would silently miss almost every real one.
/<(?:input|textarea)\b|\bf\.(?:text_field|text_area|email_field|password_field|number_field|url_field|telephone_field|phone_field|search_field|date_field)\b/- FIELD_COMPONENT =
/data-component=["'](?:input|textarea)["']|component:\s*[:"'](?:input|textarea)["']?/- FIELD_REQUIRED =
/\brequired\b/- FIELD_PLACEHOLDER =
/\bplaceholder\b/- DROPDOWN_FLIP_HINT =
/dataset\.side|["']data-side["']|setAttribute\(\s*["']data-side["']/
Instance Attribute Summary collapse
-
#findings ⇒ Object
readonly
Returns the value of attribute findings.
-
#root ⇒ Object
readonly
Returns the value of attribute root.
-
#scanned_files ⇒ Object
readonly
Returns the value of attribute scanned_files.
Instance Method Summary collapse
- #findings_for(severity) ⇒ Object
-
#initialize(root) ⇒ Doctor
constructor
A new instance of Doctor.
- #report ⇒ Object
- #run ⇒ Object
Constructor Details
#initialize(root) ⇒ Doctor
Returns a new instance of Doctor.
94 95 96 97 98 99 100 101 102 103 |
# File 'lib/maquina_components/doctor.rb', line 94 def initialize(root) @root = File.(root.to_s) @findings = [] @scanned_files = 0 # Cross-file state: some 0.7.1 rules are conclusions about the whole app, # not about one line, so they are evaluated after the scan. @destructive_tokens = {} @aria_invalid_seen = false @error_sites = [] end |
Instance Attribute Details
#findings ⇒ Object (readonly)
Returns the value of attribute findings.
92 93 94 |
# File 'lib/maquina_components/doctor.rb', line 92 def findings @findings end |
#root ⇒ Object (readonly)
Returns the value of attribute root.
92 93 94 |
# File 'lib/maquina_components/doctor.rb', line 92 def root @root end |
#scanned_files ⇒ Object (readonly)
Returns the value of attribute scanned_files.
92 93 94 |
# File 'lib/maquina_components/doctor.rb', line 92 def scanned_files @scanned_files end |
Instance Method Details
#findings_for(severity) ⇒ Object
120 121 122 |
# File 'lib/maquina_components/doctor.rb', line 120 def findings_for(severity) findings.select { |finding| finding.severity == severity } end |
#report ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/maquina_components/doctor.rb', line 124 def report out = [] out << "maquina_components doctor" out << "Scanned #{scanned_files} file#{"s" unless scanned_files == 1} under #{root}" out << "" if findings.empty? out << "No at-risk patterns found. Nothing to migrate." out << "" return out.join("\n") end SEVERITIES.each do |severity, heading| group = findings_for(severity) next if group.empty? out << "#{heading} (#{group.size})" out << "-" * 72 group.each do |finding| out << " #{relative(finding.path)}:#{finding.line} [#{finding.rule}] (#{finding.version})" out << " #{finding.source}" finding.suggestion.each_line { |line| out << " -> #{line.chomp}" } out << "" end end out << "Summary: " + SEVERITIES.keys.map { |s| "#{findings_for(s).size} #{s}" }.join(", ") out << "Advisory only - nothing was changed. See docs/upgrading.md for the release notes behind each rule." out << "" out.join("\n") end |
#run ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/maquina_components/doctor.rb', line 105 def run each_file(CSS_GLOBS) { |path| scan_css(path) } each_file(VIEW_GLOBS) { |path| scan_markup(path) scan_form_fields(path) } each_file(JS_GLOBS) { |path| scan_markup(path) scan_javascript(path) } check_destructive_palette check_invalid_without_aria self end |