Class: Clickwrap::Linter

Inherits:
Object
  • Object
show all
Defined in:
lib/clickwrap/linter.rb

Overview

Development and test heuristics for the presentation layer.

============================================================================ THESE ARE HEURISTICS. They warn; they never raise, never block a render, and never certify anything. A clean run means "none of the specific hazards below were detected in what was inspected" and nothing else.

This class must never print, return, or imply "compliant", "enforceable", "legally binding", "accessible", or "approved". Courts assess the complete page in context and accessibility applies to the whole experience, so no library that sees one fragment of one template can make any of those calls. What it can do is notice objectively checkable mistakes — a submit button above the controls, a consent box that arrives already ticked, a document nobody can open — and say so in a full sentence.

findings = Clickwrap::Linter.review_policy(Clickwrap.policy!(:signup))
findings.map(&:code)         # => [:consent_statement_bundles_purposes]
findings.first.explanation   # => a plain-English sentence

Every finding carries a stable symbol, so a test can assert on it without matching English, and an explanation, so a person reading the log knows what to do about it.

Defined Under Namespace

Classes: Finding

Constant Summary collapse

ANSWER_FIELD_PATTERN =

Rendered-fragment scans, kept to string work so they are cheap enough to run on every development render.

/name=["']clickwrap_submission\[answers\]\[([^\]"']+)\]["']/
TOKEN_FIELD_PATTERN =
/name=["']clickwrap_submission\[presentation_token\]["']/
SUBMIT_CONTROL_PATTERN =
/<(?:button|input)\b[^>]*type=["']submit["']|<button(?![^>]*type=)/i
CHECKED_ATTRIBUTE_PATTERN =
/\bchecked\b/
BUNDLED_PURPOSE_PATTERN =

A rough test for a consent sentence that is carrying more than one purpose. It is deliberately generous: the cost of a false positive is one log line suggesting a split, and the cost of a false negative is a consent record whose meaning nobody can reconstruct.

%r{\b(and|and/or|as well as|plus)\b}i

Class Method Summary collapse

Class Method Details

.enabled?Boolean

On in development and test, off everywhere else — a production request should not be scanning its own HTML — and config.lint_presentations answers for a host that disagrees with either half.

Every finding is a log line, never an exception: a lint finding is a thing to look at, not a reason to stop a developer's page from rendering. Which is exactly why it needs a switch — a warning nobody can turn off is one people learn to scroll past.

Returns:

  • (Boolean)


54
55
56
57
58
59
60
61
# File 'lib/clickwrap/linter.rb', line 54

def enabled?
  configured = Clickwrap.config.lint_presentations
  return configured unless configured.nil?

  return false unless defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env

  ::Rails.env.development? || ::Rails.env.test?
end

.review_manifest(manifest, policy:) ⇒ Object

Whether the manifest that came back still says what the policy says. A difference here means the evidence and the current server-owned offer have diverged — usually a deploy between render and submit, sometimes a hand-built form that stopped tracking the policy.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/clickwrap/linter.rb', line 99

def review_manifest(manifest, policy:)
  snapshot = manifest.respond_to?(:to_h) ? manifest.to_h : manifest
  rendered = Array(snapshot["statements"] || snapshot[:statements])
  differences = manifest_differences(rendered, policy: policy, locale: snapshot["locale"])
  return [] if differences.empty?

  [Finding.new(
    code: :rendered_manifest_differs_from_policy,
    explanation: "The submitted presentation manifest does not match the current policy " \
                 "#{policy.key}: #{differences.join("; ")}. Clickwrap verifies this at " \
                 "capture; the warning is here so the drift is visible while you can still " \
                 "explain it.",
    context: { policy: policy.key, differences: differences }
  )]
end

.review_policy(policy, locale: nil) ⇒ Object

What a compiled policy alone can be checked for, without rendering anything: blank copy, bundled consent purposes, and an optional consent that another statement has quietly made mandatory.



68
69
70
71
72
73
74
75
76
# File 'lib/clickwrap/linter.rb', line 68

def review_policy(policy, locale: nil)
  locale = (locale || (defined?(::I18n) ? ::I18n.locale : :en)).to_s

  findings = []
  findings.concat(blank_assertion_findings(policy))
  findings.concat(bundled_consent_findings(policy, locale))
  findings.concat(optional_consent_prerequisite_findings(policy))
  findings
end

.review_presentation(presentation) ⇒ Object

What one presentation can be checked for: a statement the person is asked to accept with no way to read what they are accepting.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/clickwrap/linter.rb', line 80

def review_presentation(presentation)
  presentation.statements.filter_map do |statement|
    next if statement.documents.any?
    next unless %w[agreement acknowledgment consent].include?(statement.kind)

    Finding.new(
      code: :document_link_missing,
      explanation: "The #{statement.kind} #{statement.key.inspect} presents no document, so " \
                   "the person is asked to accept something the page never shows them. Give " \
                   "the statement a `document:` and publish it.",
      context: { statement: statement.key, policy: presentation.policy_key }
    )
  end
end

.review_rendered_fields(html, presentation:) ⇒ Object

Called by the form-builder helper on every development render. Scans only the fragment it produced, so it deliberately skips the CTA-ordering check (a submit button earlier in the host's page is invisible from here — that one belongs in a system test over the whole page).



135
136
137
138
139
140
141
142
# File 'lib/clickwrap/linter.rb', line 135

def review_rendered_fields(html, presentation:)
  findings = preselected_control_findings(html.to_s)
  findings.concat(rendered_document_link_findings(html.to_s, presentation))
  findings.concat(combined_control_findings(html.to_s, presentation))
  findings.concat(review_presentation(presentation))
  warn_about(findings, source: "policy #{presentation.policy_key}")
  findings
end

.review_rendered_html(html, presentation: nil) ⇒ Object

What the rendered HTML can be checked for. Pass the whole page when you have it — the CTA-ordering check needs everything before the block, not just the block.



118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/clickwrap/linter.rb', line 118

def review_rendered_html(html, presentation: nil)
  text = html.to_s
  findings = []
  findings.concat(submit_ordering_findings(text))
  findings.concat(submit_button_text_findings(text, presentation)) if presentation
  findings.concat(preselected_control_findings(text))
  if presentation
    findings.concat(rendered_document_link_findings(text, presentation))
    findings.concat(combined_control_findings(text, presentation))
  end
  findings
end

.warn_about(findings, source: nil) ⇒ Object

Warnings go to the Rails log, or to stderr when there is no log. Never an exception: a lint finding is a thing to look at, not a reason to stop a developer's page from rendering.



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/clickwrap/linter.rb', line 147

def warn_about(findings, source: nil)
  Array(findings).each do |finding|
    message = ["[clickwrap] lint", source, "#{finding.code}: #{finding.explanation}"]
              .compact.join("")

    logger = Clickwrap.logger
    logger ? logger.warn(message) : Kernel.warn(message)
  end

  findings
end