Class: Diecut::Linter

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mill) ⇒ Linter

Returns a new instance of Linter.



5
6
7
# File 'lib/diecut/linter.rb', line 5

def initialize(mill)
  @mill = mill
end

Instance Attribute Details

#millObject (readonly)

Returns the value of attribute mill.



8
9
10
# File 'lib/diecut/linter.rb', line 8

def mill
  @mill
end

Instance Method Details

#each_defaultObject



37
38
39
40
41
42
43
# File 'lib/diecut/linter.rb', line 37

def each_default
  each_plugin do |plugin|
    plugin.context_defaults.each do |default|
      yield default, plugin
    end
  end
end

#each_optionObject



45
46
47
48
49
50
51
# File 'lib/diecut/linter.rb', line 45

def each_option
  each_plugin do |plugin|
    plugin.options.each do |option|
      yield option, plugin
    end
  end
end

#each_pluginObject



31
32
33
34
35
# File 'lib/diecut/linter.rb', line 31

def each_plugin
  mill.mediator.activated_plugins.each do |plugin|
    yield plugin
  end
end

#option_collision_reportObject



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
# File 'lib/diecut/linter.rb', line 100

def option_collision_report
  Report.new("Option collisions", ["Output target", "Option name", "Source plugin"]).tap do |report|
    option_targets = Hash.new{|h,k| h[k]=[]}
    each_option do |option, plugin|
      next unless option.has_context_path?
      option_targets[option.context_path] << [plugin, option]
    end
    option_targets.each_value do |set|
      if set.length > 1
        set.each do |plugin, option|
          report.add(option.context_path.join("."), option.name, plugin.name)
        end
      end
    end

    unless report.empty?
      report.fail("Multiple options assign the same values to be rendered")
      report.advice = unindent(<<-EOA)
      This is problem because two options in the user interface both change
      rendered values. If a user supplies both with different values, the
      output isn't predictable (either one might take effect).

      Most likely, this is a simple error: remove options from each group
      that targets the same rendered value until only one remains. It may
      also be that one option has a typo - that there's a rendering target
      that's omitted.
      EOA
    end
  end
end

#orphaned_fieldsObject



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/diecut/linter.rb', line 131

def orphaned_fields
  Report.new("Template fields all have settings", ["Output field", "Source file"]).tap do |report|
    context_class = mill.context_class

    required_fields = {}

    context_class.field_names.each do |field_name|
      if context_class.(field_name).is?(:required)
        required_fields[field_name.to_s] = []
      end
    end

    mill.templates.all_templates.each do |template|
      template.reduced.leaf_fields.each do |field|
        field = field.join(".")
        if required_fields.has_key?(field)
          required_fields[field] << template.path
        end
      end
    end

    each_option do |option, plugin|
      next unless option.has_context_path?
      field = option.context_path.join(".")
      required_fields.delete(field)
    end

    required_fields.each do |name, targets|
      targets.each do |target|
        report.add(name, target)
      end
    end

    unless report.empty?
      report.status = "WARN"
      report.advice = unindent(<<-EOA)
      These fields might not receive a value during generation, which will
      raise an error at use time.

      It's possible these fields are set in a resolve block in one of the
      plugins - Diecut can't check for that yet.
      EOA
    end
  end
end

#overridden_context_defaultsObject



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
# File 'lib/diecut/linter.rb', line 53

def overridden_context_defaults
  Report.new("Overridden context defaults", ["Output field", "Default value", "Source plugin"]).tap do |report|
    default_values = Hash.new{|h,k| h[k]=[]}
    each_default do |default, plugin|
      next unless default.simple?

      default_values[default.context_path] << [default, plugin]
    end

    default_values.each do |key, set|
      default_values[key] = set.find_all do |plugin|
        !set.any?{|child|
          next if child == plugin
          Diecut.plugin_loader.strict_sequence?(plugin[1], child[1])
        }
      end
    end

    default_values.each_value do |set|
      if set.length > 1
        set.each do |default, plugin|

          report.add(default.context_path.join("."), default.value, plugin.name)
        end
      end
    end

    unless report.empty?
      report.fail("Multiple plugins assign different values to be rendered")
      report.advice = unindent(<<-EOA)
      This is a problem because each plugin may be assuming it's default
      value, and since there's no guarantee in which order the plugins are
      loaded, the actual default value is difficult to predict. In general,
      this kind of override behavior can be difficult to reason about.

      Either the collision is accidental, in which case the default value
      should be removed from one plugin or the other. If the override is
      intentional, then the overriding plugin's gem should depend on the
      overridden one's - since you are overriding the value intentionally,
      it makes sense to ensure that the value is there to override. Diecut
      will load plugins such that the dependant plugins are loaded later,
      which solves the predictability problem.
      EOA
    end
  end
end

#reportObject



10
11
12
13
14
15
16
17
18
19
20
# File 'lib/diecut/linter.rb', line 10

def report
  @ui = mill.user_interface

  formatter = ReportFormatter.new([
    option_collision_report,
    orphaned_fields,
    overridden_context_defaults
  ])

  formatter.to_s
end

#unindent(text) ⇒ Object

Needed: Overridden option defaults (without plugin dep) Option with default, context with default (w/o PD)



26
27
28
29
# File 'lib/diecut/linter.rb', line 26

def unindent(text)
  indent = text.scan(/(^[ \t]*)\S/).map{|cap| cap.first}.max_by(&:length)
  text.gsub(%r{^#{indent}},'')
end