Created
November 9, 2013 19:25
-
-
Save denisdefreyne/7388847 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# encoding: utf-8 | |
Bundler.require | |
require 'set' | |
require 'parser/current' | |
module NanocX | |
############################################################################# | |
class FullSubset | |
def self.instance | |
@instance ||= NanocX::FullSubset.new | |
end | |
def +(other) | |
self | |
end | |
def inspect | |
"<FullSubset>" | |
end | |
end | |
class EnumeratedSubset | |
attr_reader :elements | |
def self.empty | |
@empty ||= NanocX::EnumeratedSubset.new([]) | |
end | |
def initialize(elements) | |
@elements = Set.new(elements) | |
end | |
def +(other) | |
case other | |
when EnumeratedSubset | |
EnumeratedSubset.new(self.elements + other.elements) | |
else | |
other + self | |
end | |
end | |
def inspect | |
"<EnumeratedSubset(#{@elements.map { |e| e.inspect }.join(', ')})>" | |
end | |
end | |
class GlobSubset | |
attr_reader :excluded_globs | |
attr_reader :included_globs | |
def initialize(excluded_globs, included_globs) | |
@excluded_globs = excluded_globs | |
@included_globs = included_globs | |
end | |
def +(other) | |
case other | |
when GlobSubset | |
raise 'Cannot add GlobSubset to GlobSubset' | |
when EnumeratedSubset | |
CombinedSubset.new(self, other) | |
else | |
other + self | |
end | |
end | |
def inspect | |
"<GlobSubset(excluded=#{@excluded_globs.inspect} included=#{@included_globs.inspect})>" | |
end | |
end | |
class CombinedSubset | |
attr_reader :glob_subset | |
attr_reader :enumerated_subset | |
def initialize(glob_subset, enumerated_subset) | |
@glob_subset = glob_subset | |
@enumerated_subset = enumerated_subset | |
end | |
def +(other) | |
case other | |
when GlobSubset | |
CombinedSubset.new(@glob_subset + other, @enumerated_subset) | |
when EnumeratedSubset | |
CombinedSubset.new(@glob_subset, @enumerated_subset + other) | |
else | |
other + self | |
end | |
end | |
def inspect | |
"<CombinedSubset(globs=#{@glob_subset.inspect} enumerateds=#{@enumerated_subset.inspect})>" | |
end | |
end | |
############################################################################# | |
class RulesAnalyser | |
# @return [Array] list of tuples like [:compile, [args], checksum] | |
def analyse(string) | |
root_node = ::Parser::CurrentRuby.parse(string) | |
raise 'Not begin' if root_node.type != :begin | |
list = root_node.children.map do |child_node| | |
case child_node.type | |
when :block # preprocess compile | |
handle_block(child_node) | |
when :send | |
handle_send(child_node) | |
end | |
end | |
hashify(list.compact) | |
end | |
private | |
def hashify(list) | |
list.each_with_object({}) do |entry, memo| | |
type = entry[0] | |
args = entry[1] | |
checksum = entry[2] | |
memo[type] ||= [] | |
memo[type] << [ args, checksum ] | |
end | |
end | |
def node_to_string(node) | |
case node | |
when AST::Node | |
'(' + | |
node.type.to_s + | |
node.children.map { |c| ' ' + node_to_string(c) }.join + | |
')' | |
else | |
node.inspect | |
end | |
end | |
# preprocess, compile | |
def handle_block(node) | |
send_node = node.children[0] | |
return nil if send_node.type != :send | |
return nil if send_node.children[0] != nil | |
valid = [ :preprocess, :compile ] | |
meth_name = send_node.children[1] | |
return nil unless valid.include?(meth_name) | |
if meth_name == :preprocess && send_node.children.length != 2 | |
raise '#preprocess not called with 0 params' | |
elsif meth_name == :compile && send_node.children.length != 3 | |
raise '#compile not called with 1 param' | |
end | |
if meth_name == :preprocess | |
args = [] | |
elsif meth_name == :compile | |
pattern_node = send_node.children[2] | |
if pattern_node.type != :str | |
raise '#compile not called with a string as its first param' | |
end | |
args = pattern_node.children | |
end | |
args_node = node.children[1] | |
raise 'args is not second child' if args_node.type != :args | |
if args_node.children.length > 0 | |
raise '#compile/#preprocess not called with zero node params' | |
end | |
begin_node = node.children[2] | |
[ meth_name, args, node_to_string(begin_node).checksum ] | |
end | |
# layout | |
def handle_send(node) | |
if node.children[1] != :layout | |
return nil | |
end | |
if node.children.length != 4 | |
raise '#layout not called with right amount of params (expected 2)' | |
end | |
if node.children[0] != nil | |
raise '#layout not called without explicit receiver' | |
end | |
pattern_node = node.children[2] | |
if pattern_node.type != :str | |
raise '#layout not called with a string as its first param' | |
end | |
pattern = pattern_node.children[0] | |
sym_node = node.children[3] | |
if sym_node.type != :sym | |
raise '#layout not called with a symbol as its second param' | |
end | |
sym = sym_node.children[0] | |
[ :layout, [ pattern ], sym ] | |
end | |
end | |
############################################################################# | |
class ConfigChange | |
end | |
class RulesChange | |
attr_reader :type | |
attr_reader :excluded_globs | |
attr_reader :included_globs | |
def initialize(type, excluded_globs, included_globs) | |
@type = type | |
@excluded_globs = excluded_globs | |
@included_globs = included_globs | |
end | |
end | |
class ChangeCollection | |
attr_accessor :config_change | |
attr_accessor :item_changes | |
attr_accessor :layout_changes | |
attr_accessor :code_snippet_changes | |
attr_accessor :rules_changes | |
def initialize | |
@config_change = nil | |
@item_changes = [] # identifiers | |
@layout_changes = [] # identifiers | |
@code_snippet_changes = [] # filenames | |
@rules_changes = [] | |
end | |
end | |
class ChangesFinder | |
def initialize(site, timestamp) | |
@site = site | |
@timestamp = timestamp | |
end | |
def run | |
NanocX::ChangeCollection.new.tap do |changes| | |
data_sources = @site.data_sources | |
changes.config_change = self.config_change | |
changes.item_changes = self.item_changes | |
changes.layout_changes = self.layout_changes | |
changes.rules_changes = self.rules_changes | |
end | |
end | |
def config_change | |
if File.mtime(Nanoc::SiteLoader::CONFIG_FILENAME) >= @timestamp | |
NanocX::ConfigChange.new | |
else | |
nil | |
end | |
end | |
def item_changes | |
@site.data_sources.flat_map { |ds| ds.item_filenames_changed_since(@timestamp) } | |
end | |
def layout_changes | |
@site.data_sources.flat_map { |ds| ds.layout_filenames_changed_since(@timestamp) } | |
end | |
def rules_changes | |
# Load/analyse rules | |
current_rules = RulesAnalyser.new.analyse(File.read('Rules')) | |
# Load previous checksums | |
checksum_store = Nanoc::RulesChecksumStore.new.tap { |s| s.load } | |
checksum_store.add(:compile, ["/stylesheet.*"], "9af871b5ca3b4ed7fdfdb611a5fac8e925b65175") | |
checksum_store.add(:compile, ["/**/*"], "42cc271f62249c244662475fee459fed9737cd09") | |
# Find included/excluded globs | |
[ :compile, :preprocess, :layout ].map do |type| | |
a = checksum_store.all[type] || [] | |
b = current_rules[type] || [] | |
max = [ a.size, b.size ].max.times do |i| | |
break i if a[i] != b[i] | |
end | |
excluded = (max && max > 0 ? b[0..max-1] : []).map { |e| e[0] } | |
included = (max ? b[max..-1] : []).map { |e| e[0] } | |
RulesChange.new(type, excluded, included) | |
end | |
end | |
end | |
############################################################################# | |
class ConfigModificationOutdatednessChecker | |
def run(items, layouts, changes) | |
if changes.config_change.nil? | |
EnumeratedSubset.empty | |
else | |
FullSubset.instance | |
end | |
end | |
end | |
class ItemModificationOutdatednessChecker | |
def run(items, layouts, changes) | |
EnumeratedSubset.new( | |
changes.item_changes.each_with_object(Set.new) do |ic, memo| | |
# FIXME check in O(1) | |
if items.any? { |i| i.identifier == ic } | |
memo << ic | |
end | |
end | |
) | |
end | |
end | |
class CodeSnippetModificationOutdatednessChecker | |
def run(items, layouts, changes) | |
# FIXME compare with checksums if it is not empty | |
if changes.code_snippet_changes.empty? | |
EnumeratedSubset.empty | |
else | |
FullSubset.instance | |
end | |
end | |
end | |
class RulesModificationOutdatednessChecker | |
def run(items, layouts, changes) | |
rc = changes.rules_changes.find { |e| e.type == :compile } | |
if rc.included_globs.empty? | |
EnumeratedSubset.empty | |
else | |
GlobSubset.new(rc.excluded_globs, rc.included_globs) | |
end | |
end | |
end | |
class DependencyOutdatednessChecker | |
def run(items, layouts, changes) | |
dependency_tracker = Nanoc::DependencyTracker.new(items + layouts).tap { |s| s.load } | |
changed_items = items.select { |i| changes.item_changes.include?(i.identifier.to_s) } | |
changed_layouts = layouts.select { |l| changes.layout_changes.include?(l.identifier.to_s) } | |
changed_objs = changed_items + changed_layouts | |
all_changed_objs = changed_objs.flat_map { |e| dependency_tracker.all_objects_outdated_due_to(e) } | |
EnumeratedSubset.new(all_changed_objs.select { |e| Nanoc::Item === e}.map { |e| e.identifier }) | |
end | |
end | |
############################################################################# | |
class ItemOutdatednessChecker | |
def run(items, layouts, changes) | |
classes = [ | |
ConfigModificationOutdatednessChecker, | |
CodeSnippetModificationOutdatednessChecker, | |
ItemModificationOutdatednessChecker, | |
RulesModificationOutdatednessChecker, | |
DependencyOutdatednessChecker | |
] | |
outdated_items = classes.inject(EnumeratedSubset.empty) do |memo, klass| | |
case memo | |
when FullSubset | |
memo | |
when EnumeratedSubset, GlobSubset, CombinedSubset | |
memo + klass.new.run(items, layouts, changes) | |
end | |
end | |
# TODO convert from FullSubset to actual collection of items | |
outdated_items | |
end | |
end | |
class LayoutOutdatednessChecker | |
def run(layouts, changes) | |
# TODO implement | |
end | |
end | |
end | |
############################################################################### | |
def log(msg) | |
puts "[#{Time.now.strftime("%H:%M:%S.%L")}] #{msg}" | |
end | |
############################################################################### | |
log("Loading site") | |
site = Nanoc::SiteLoader.new.load | |
data_source = Nanoc::DataSources::Filesystem.new('/', '/', {}) | |
log("Finding changes") | |
timestamp = Time.now - 100 | |
changes = NanocX::ChangesFinder.new(site, timestamp).run | |
puts | |
puts "Config:" ; print ' ' | |
p NanocX::ConfigModificationOutdatednessChecker.new.run(data_source.items, data_source.layouts, changes) | |
puts | |
puts "Item modification:" ; print ' ' | |
p NanocX::ItemModificationOutdatednessChecker.new.run(data_source.items, data_source.layouts, changes) | |
puts | |
puts "Code snippet modification:" ; print ' ' | |
p NanocX::CodeSnippetModificationOutdatednessChecker.new.run(data_source.items, data_source.layouts, changes) | |
puts | |
puts "Dependency:" ; print ' ' | |
p NanocX::DependencyOutdatednessChecker.new.run(data_source.items, data_source.layouts, changes) | |
puts | |
puts "Rules:" ; print ' ' | |
p NanocX::RulesModificationOutdatednessChecker.new.run(data_source.items, data_source.layouts, changes) | |
puts | |
puts "All together:" ; print ' ' | |
p NanocX::ItemOutdatednessChecker.new.run(data_source.items,data_source.layouts, changes) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment