Skip to content

Instantly share code, notes, and snippets.

@denisdefreyne
Created November 9, 2013 19:25
Show Gist options
  • Save denisdefreyne/7388847 to your computer and use it in GitHub Desktop.
Save denisdefreyne/7388847 to your computer and use it in GitHub Desktop.
# 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