lib/faml/stats.rb
# frozen_string_literal: true
require 'find'
require 'pathname'
require 'haml_parser/parser'
require_relative 'static_hash_parser'
module Faml
class Stats
Info = Struct.new(
:empty_attribute_count,
:static_attribute_count,
:static_id_or_class_attribute_count,
:dynamic_attribute_count,
:dynamic_attribute_with_data_count,
:dynamic_attribute_with_newline_count,
:runtime_attribute_count,
:object_reference_count,
:multi_attribute_count,
:ast_types
) do
def initialize(*)
super
self.ast_types ||= Hash.new { |h, k| h[k] = 0 }
members.each do |k|
self[k] ||= 0
end
end
end
def initialize(*paths)
@files = find_files(paths)
end
def report
info = Info.new
@files.each do |file|
collect_info(info, file)
end
report_attribute_stats(info)
report_ast_stats(info)
end
private
def find_files(paths)
paths.flat_map do |path|
if File.directory?(path)
find_haml_files(path)
else
[path.to_s]
end
end
end
def find_haml_files(dir)
files = []
Find.find(dir) do |file|
if File.extname(file) == '.haml'
files << file
end
end
files
end
def collect_info(info, file)
ast = HamlParser::Parser.new(filename: file).call(File.read(file))
walk_ast(info, ast)
end
def walk_ast(info, ast)
info.ast_types[ast.class.to_s.sub(/\A.*::(.+)\z/, '\1')] += 1
case ast
when HamlParser::Ast::Root
ast.children.each { |c| walk_ast(info, c) }
when HamlParser::Ast::Doctype
:noop
when HamlParser::Ast::Element
collect_attribute_info(info, ast)
if ast.oneline_child
walk_ast(info, ast.oneline_child)
end
ast.children.each { |c| walk_ast(info, c) }
when HamlParser::Ast::Script
ast.children.each { |c| walk_ast(info, c) }
when HamlParser::Ast::SilentScript
ast.children.each { |c| walk_ast(info, c) }
when HamlParser::Ast::HtmlComment
ast.children.each { |c| walk_ast(info, c) }
when HamlParser::Ast::HamlComment
ast.children.each { |c| walk_ast(info, c) }
when HamlParser::Ast::Text
:noop
when HamlParser::Ast::Filter
:noop
when HamlParser::Ast::Empty
:noop
else
raise "InternalError: Unknown ast #{ast.class}: #{ast.inspect}"
end
end
def collect_attribute_info(info, ast)
if ast.object_ref
info.object_reference_count += 1
return
end
if !ast.old_attributes && !ast.new_attributes
if ast.static_class.empty? && ast.static_id.empty?
info.empty_attribute_count += 1
else
info.static_id_or_class_attribute_count += 1
end
else
static_hash_parser = StaticHashParser.new
if static_hash_parser.parse("{#{ast.new_attributes}#{ast.old_attributes}}")
if static_hash_parser.dynamic_attributes.empty?
info.static_attribute_count += 1
elsif static_hash_parser.dynamic_attributes.key?('data') || static_hash_parser.dynamic_attributes.key?(:data)
info.dynamic_attribute_with_data_count += 1
elsif ast.old_attributes && ast.old_attributes.include?("\n")
info.dynamic_attribute_with_newline_count += 1
elsif ast.new_attributes && ast.new_attributes.include?("\n")
info.dynamic_attribute_with_newline_count += 1
else
info.dynamic_attribute_count += 1
end
else
call_ast = Parser::CurrentRuby.parse("call(#{ast.new_attributes}#{ast.old_attributes})")
if call_ast.type == :send && call_ast.children[0].nil? && call_ast.children[1] == :call && !call_ast.children[3].nil?
info.multi_attribute_count += 1
else
info.runtime_attribute_count += 1
end
end
end
end
def report_attribute_stats(info)
static = info.static_attribute_count
dynamic = info.dynamic_attribute_count + info.dynamic_attribute_with_data_count + info.dynamic_attribute_with_newline_count
runtime = info.runtime_attribute_count + info.multi_attribute_count + info.object_reference_count
total = static + dynamic + runtime
puts 'Attribute stats'
printf(" Empty attributes: %d\n", info.empty_attribute_count)
printf(" Attributes with id or class only: %d\n", info.static_id_or_class_attribute_count)
printf(" Static attributes: %d (%.2f%%)\n", static, static * 100.0 / total)
printf(" Dynamic attributes: %d (%.2f%%)\n", dynamic, dynamic * 100.0 / total)
printf(" with data: %d\n", info.dynamic_attribute_with_data_count)
printf(" with newline: %d\n", info.dynamic_attribute_with_newline_count)
printf(" Runtime attributes: %d (%.2f%%)\n", runtime, runtime * 100.0 / total)
printf(" with multiple arguments: %d\n", info.multi_attribute_count)
printf(" with object reference: %d\n", info.object_reference_count)
end
def report_ast_stats(info)
total = info.ast_types.values.inject(0, :+)
puts 'AST stats'
info.ast_types.keys.sort.each do |type|
v = info.ast_types[type]
printf(" %s: %d (%.2f%%)\n", type, v, v * 100.0 / total)
end
end
end
end