From eda4c046ad359b8fef2b495562e342d78de1d942 Mon Sep 17 00:00:00 2001 From: vicxu Date: Thu, 21 Mar 2019 20:40:38 +0800 Subject: [PATCH 01/14] fix LocalGitRepo#folder_structure --- .gitignore | 3 ++- app/infrastructure/git/local_repo.rb | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8cc1a3f..e6d9610 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ coverage/ *.db .bundle/ _snippets/ -_cache/ \ No newline at end of file +_cache/ +.ruby-version \ No newline at end of file diff --git a/app/infrastructure/git/local_repo.rb b/app/infrastructure/git/local_repo.rb index a06b4e1..853a786 100644 --- a/app/infrastructure/git/local_repo.rb +++ b/app/infrastructure/git/local_repo.rb @@ -45,6 +45,7 @@ def folder_structure parts = full_path.split('/') parent = parts.length.equal?(1) ? '/' : parts[0..-2].join('/') (structure[parent] ||= []).push(full_path) + structure end end end From 621d3c879c079b16477e5d253f9f04c615610743 Mon Sep 17 00:00:00 2001 From: vicxu Date: Thu, 21 Mar 2019 22:39:36 +0800 Subject: [PATCH 02/14] create factory bot for project and member object --- Gemfile | 1 + Gemfile.lock | 12 ++++++++++++ app/infrastructure/database/orms/init.rb | 6 ++++++ spec/factories/init.rb | 6 ++++++ spec/factories/member_factory.rb | 8 ++++++++ spec/factories/project_factory.rb | 20 ++++++++++++++++++++ spec/helpers/spec_helper.rb | 3 +++ 7 files changed, 56 insertions(+) create mode 100644 spec/factories/init.rb create mode 100644 spec/factories/member_factory.rb create mode 100644 spec/factories/project_factory.rb diff --git a/Gemfile b/Gemfile index 2aa3a1b..4340db7 100644 --- a/Gemfile +++ b/Gemfile @@ -40,6 +40,7 @@ gem 'sequel', '~> 5.13' group :development, :test do gem 'database_cleaner' gem 'sqlite3' + gem 'factory_bot', '~> 5.0', '>= 5.0.2' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 439ea16..01edf4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,6 +1,11 @@ GEM remote: https://rubygems.org/ specs: + activesupport (5.2.2.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) ast (2.4.0) @@ -100,6 +105,8 @@ GEM ethon (0.11.0) ffi (>= 1.3.0) eventmachine (1.2.7) + factory_bot (5.0.2) + activesupport (>= 4.2.0) faraday (0.15.4) multipart-post (>= 1.2, < 3) faraday_middleware (0.12.2) @@ -139,6 +146,8 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_parser.rb (0.6.0) + i18n (1.6.0) + concurrent-ruby (~> 1.0) ice_nine (0.11.2) interception (0.5) jaro_winkler (1.5.1) @@ -249,6 +258,8 @@ GEM typhoeus (~> 0.6, >= 0.6.8) typhoeus (0.8.0) ethon (>= 0.8.0) + tzinfo (1.2.5) + thread_safe (~> 0.1) uber (0.1.0) unf (0.1.4) unf_ext @@ -281,6 +292,7 @@ DEPENDENCIES dry-types (~> 0.5) dry-validation econfig (~> 2.1) + factory_bot (~> 5.0, >= 5.0.2) faye (~> 1) flog hirb (~> 0.7) diff --git a/app/infrastructure/database/orms/init.rb b/app/infrastructure/database/orms/init.rb index 50d81c0..035cc64 100644 --- a/app/infrastructure/database/orms/init.rb +++ b/app/infrastructure/database/orms/init.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +class Sequel::Model + def save! + self.save && true + end +end + Dir.glob("#{File.dirname(__FILE__)}/*.rb").each do |file| require file end diff --git a/spec/factories/init.rb b/spec/factories/init.rb new file mode 100644 index 0000000..58357b5 --- /dev/null +++ b/spec/factories/init.rb @@ -0,0 +1,6 @@ +require 'factory_bot' +require_relative '../../app/infrastructure/database/orms/init' + +Dir.glob("#{File.dirname(__FILE__)}/*_factory.rb").each do |file| + require file +end diff --git a/spec/factories/member_factory.rb b/spec/factories/member_factory.rb new file mode 100644 index 0000000..fe7b21e --- /dev/null +++ b/spec/factories/member_factory.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :member, class: "CodePraise::Database::MemberOrm" do + origin_id { 11512564 } + username { "XuVic" } + email { "xumingyo@gmail.com" } + initialize_with { CodePraise::Database::MemberOrm.find(origin_id: 11512564) || CodePraise::Database::MemberOrm.create(attributes) } + end +end \ No newline at end of file diff --git a/spec/factories/project_factory.rb b/spec/factories/project_factory.rb new file mode 100644 index 0000000..128fca3 --- /dev/null +++ b/spec/factories/project_factory.rb @@ -0,0 +1,20 @@ +require_relative 'member_factory' + +FactoryBot.define do + factory :project, class: "CodePraise::Database::ProjectOrm" do + origin_id { 126318191 } + name {"alpha-blog"} + size { 7181 } + ssh_url { "git://github.com/XuVic/alpha-blog.git" } + http_url { "https://github.com/XuVic/alpha-blog" } + association :owner, factory: :member + initialize_with { CodePraise::Database::ProjectOrm.find(origin_id: 126318191) || CodePraise::Database::ProjectOrm.create(attributes) } + + factory :project_with_contributor do + after(:create) do |project| + contributor = FactoryBot.create(:member) + project.add_contributor(contributor) + end + end + end +end \ No newline at end of file diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index c1cd95d..e79036e 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -20,3 +20,6 @@ GITHUB_TOKEN = CodePraise::Api.config.GITHUB_TOKEN CORRECT = YAML.safe_load(File.read('spec/fixtures/gh_results.yml')) +class Minitest::Spec + include FactoryBot::Syntax::Methods +end From f25f3697dcac37d5503e4adffe996de76224f6d5 Mon Sep 17 00:00:00 2001 From: vicxu Date: Fri, 22 Mar 2019 15:31:29 +0800 Subject: [PATCH 03/14] create complexity and idiomaticity metrics --- .../entities/children/file_contributions.rb | 6 ++- .../models/contributions/entities/init.rb | 2 +- .../entities/metrics/complexity.rb | 39 +++++++++++++++++++ .../entities/metrics/idiomaticity.rb | 39 +++++++++++++++++++ .../contributions/entities/metrics/init.rb | 5 +++ .../entities/root/folder_contributions.rb | 7 ++-- .../mappers/contributions_mapper.rb | 3 +- .../mappers/file_contributions_mapper.rb | 17 +++++++- .../mappers/folder_contributions_mapper.rb | 9 +++-- spec/helpers/spec_helper.rb | 2 + .../quality_measurement_spec.rb | 26 +++++++++++++ 11 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 app/domain/models/contributions/entities/metrics/complexity.rb create mode 100644 app/domain/models/contributions/entities/metrics/idiomaticity.rb create mode 100644 app/domain/models/contributions/entities/metrics/init.rb create mode 100644 spec/tests_integration/measurement_integration/quality_measurement_spec.rb diff --git a/app/domain/models/contributions/entities/children/file_contributions.rb b/app/domain/models/contributions/entities/children/file_contributions.rb index 2b6311b..8570560 100644 --- a/app/domain/models/contributions/entities/children/file_contributions.rb +++ b/app/domain/models/contributions/entities/children/file_contributions.rb @@ -11,11 +11,13 @@ class FileContributions WANTED_EXTENSION = %w[rb js css html slim md].join('|') EXTENSION_REGEX = /#{DOT}#{WANTED_EXTENSION}#{LINE_END}/.freeze - attr_reader :file_path, :lines + attr_reader :file_path, :lines, :complexity, :idiomaticity - def initialize(file_path:, lines:) + def initialize(file_path:, lines:, complexity:, idiomaticity:) @file_path = Value::FilePath.new(file_path) @lines = lines + @complexity = complexity + @idiomaticity = idiomaticity end def total_credits diff --git a/app/domain/models/contributions/entities/init.rb b/app/domain/models/contributions/entities/init.rb index 7b02065..78f9159 100644 --- a/app/domain/models/contributions/entities/init.rb +++ b/app/domain/models/contributions/entities/init.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -folders = %w[children root] +folders = %w[metrics children root] folders.each do |folder| require_relative "#{folder}/init.rb" end diff --git a/app/domain/models/contributions/entities/metrics/complexity.rb b/app/domain/models/contributions/entities/metrics/complexity.rb new file mode 100644 index 0000000..852fec5 --- /dev/null +++ b/app/domain/models/contributions/entities/metrics/complexity.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'flog' + +module CodePraise + + module Entity + + class Complexity + + def initialize(file_path:) + return nil unless ruby_file?(file_path) + @flog = Flog.new + @flog.flog(*file_path) + end + + def average + @flog.average + end + + def methods_score + methods_score = @flog.totals + methods_score.keys.inject({}) do |result, file_path| + result[method_name(file_path)] = @flog.totals[file_path] + result + end + end + + private + + def ruby_file?(file_path) + File.extname(file_path) == '.rb' + end + + def method_name(file_path) + file_path.split("#").last + end + end + end +end diff --git a/app/domain/models/contributions/entities/metrics/idiomaticity.rb b/app/domain/models/contributions/entities/metrics/idiomaticity.rb new file mode 100644 index 0000000..e128fd2 --- /dev/null +++ b/app/domain/models/contributions/entities/metrics/idiomaticity.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +require 'rubocop' + +module CodePraise + + module Entity + + class Idiomaticity + + def initialize(file_path:) + return nil unless ruby_file?(file_path) + process_source = RuboCop::ProcessedSource.from_file(file_path, ruby_version) + config = RuboCop::ConfigStore.new.for(process_source.path) + cop_classes = RuboCop::Cop::Cop.all + registry = RuboCop::Cop::Registry.new(cop_classes) + team = RuboCop::Cop::Team.new(registry, config) + @result = team.inspect_file(process_source) + end + + def count + @result.count + end + + def messages + @result.map(&:message) + end + + private + + def ruby_file?(file_path) + File.extname(file_path) == '.rb' + end + + def ruby_version + RUBY_VERSION.to_f + end + end + end +end diff --git a/app/domain/models/contributions/entities/metrics/init.rb b/app/domain/models/contributions/entities/metrics/init.rb new file mode 100644 index 0000000..0019f37 --- /dev/null +++ b/app/domain/models/contributions/entities/metrics/init.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Dir.glob("#{__dir__}/*.rb").each do |file| + require file +end diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index c7618f3..f2aa58f 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -6,11 +6,12 @@ module Entity class FolderContributions < SimpleDelegator include Mixins::ContributionsCalculator - attr_reader :path, :files + attr_reader :path, :files, :repo_path - def initialize(path:, files:) + def initialize(path:, files:, repo_path:) @path = path @files = files + @repo_path = repo_path super(Types::HashedArrays.new) base_files.each { |file| self[file.file_path.filename] = file } @@ -45,7 +46,7 @@ def subfolders end @subfolders = folders.map do |folder_name, folder_files| - FolderContributions.new(path: folder_name, files: folder_files) + FolderContributions.new(path: folder_name, files: folder_files, repo_path: @repo_path) end end diff --git a/app/domain/models/contributions/mappers/contributions_mapper.rb b/app/domain/models/contributions/mappers/contributions_mapper.rb index babcc61..8ceb3de 100644 --- a/app/domain/models/contributions/mappers/contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/contributions_mapper.rb @@ -13,7 +13,8 @@ def for_folder(folder_name) Mapper::FolderContributions.new( folder_name, - parse_file_reports(blame) + parse_file_reports(blame), + @gitrepo.local.git_repo_path ).build_entity end diff --git a/app/domain/models/contributions/mappers/file_contributions_mapper.rb b/app/domain/models/contributions/mappers/file_contributions_mapper.rb index aa99136..20b7f34 100644 --- a/app/domain/models/contributions/mappers/file_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/file_contributions_mapper.rb @@ -4,14 +4,17 @@ module CodePraise module Mapper # Summarizes a single file's contributions by team members class FileContributions - def initialize(file_report) + def initialize(file_report, repo_path) @file_report = file_report + @repo_path = repo_path end def build_entity Entity::FileContributions.new( file_path: filename, - lines: contributions + lines: contributions, + complexity: complexity, + idiomaticity: idiomaticity ) end @@ -25,6 +28,16 @@ def contributions summarize_line_reports(@file_report[1]) end + def complexity + file_path = Value::FilePath.new(filename) + Entity::Complexity.new(file_path: "#{@repo_path}/#{file_path}") + end + + def idiomaticity + file_path = Value::FilePath.new(filename) + Entity::Idiomaticity.new(file_path: "#{@repo_path}/#{file_path}") + end + def summarize_line_reports(line_reports) line_reports.map.with_index do |report, line_index| Entity::LineContribution.new( diff --git a/app/domain/models/contributions/mappers/folder_contributions_mapper.rb b/app/domain/models/contributions/mappers/folder_contributions_mapper.rb index 2153dd9..d00f487 100644 --- a/app/domain/models/contributions/mappers/folder_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/folder_contributions_mapper.rb @@ -8,22 +8,25 @@ module Mapper class FolderContributions attr_reader :folder_name attr_reader :contributions_reports + attr_reader :repo_path - def initialize(folder_name, contributions_reports) + def initialize(folder_name, contributions_reports, repo_path) @folder_name = folder_name @contributions_reports = contributions_reports + @repo_path = repo_path end def build_entity Entity::FolderContributions.new( path: @folder_name, - files: file_summaries + files: file_summaries, + repo_path: @repo_path ) end def file_summaries @contributions_reports.map do |file_report| - Mapper::FileContributions.new(file_report).build_entity + Mapper::FileContributions.new(file_report, @repo_path).build_entity end end diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index e79036e..e8b3703 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -12,7 +12,9 @@ require 'pry' # for debugging +require 'factory_bot' require_relative '../../init.rb' +require_relative '../factories/init' USERNAME = 'soumyaray' PROJECT_NAME = 'YPBT-app' diff --git a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb b/spec/tests_integration/measurement_integration/quality_measurement_spec.rb new file mode 100644 index 0000000..a14c640 --- /dev/null +++ b/spec/tests_integration/measurement_integration/quality_measurement_spec.rb @@ -0,0 +1,26 @@ +require_relative '../../helpers/spec_helper.rb' + +describe "Test Quality Measurement" do + + before do + project = create(:project) + git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) + folder = CodePraise::Mapper::Contributions.new(git_repo).for_folder('app') + @file = folder.files[0] + end + + describe "Entity::Complexity" do + it "should return complexity score" do + _(@file.complexity.average).wont_be_nil + _(@file.complexity.methods_score.keys).wont_be_empty + end + end + + describe "Entity::Idiomaticity" do + it "should count number of unidiomatic code" do + _(@file.idiomaticity.count).wont_be_nil + _(@file.idiomaticity.messages).wont_be_empty + binding.pry + end + end +end \ No newline at end of file From 39472f211360e31f77b6920d3d1fa508ef7a1d31 Mon Sep 17 00:00:00 2001 From: vicxu Date: Sun, 24 Mar 2019 23:19:27 +0800 Subject: [PATCH 04/14] create representer and entity for complexity, idiomaticity and function contribution --- Gemfile | 3 + Gemfile.lock | 20 +++++ .../entities/children/file_contributions.rb | 5 +- .../entities/children/method_contribution.rb | 29 +++++++ .../entities/metrics/complexity.rb | 11 ++- .../entities/metrics/idiomaticity.rb | 6 +- .../entities/root/folder_contributions.rb | 22 ++++++ .../lib/contributions_calculator.rb | 4 + .../mappers/file_contributions_mapper.rb | 14 +++- .../mappers/method_contribution_mapper.rb | 75 +++++++++++++++++++ .../contributions/repositories/git_repo.rb | 2 +- .../contributions/values/credit_share.rb | 9 +++ .../representers/complexity_representer.rb | 15 ++++ .../file_contributions_representer.rb | 7 ++ .../representers/idiomaticity_representer.rb | 14 ++++ .../method_complexity_representer.rb | 14 ++++ .../method_contributions_representer.rb | 19 +++++ .../productivity_measurement.rb | 20 +++++ .../quality_measurement_spec.rb | 2 +- 19 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 app/domain/models/contributions/entities/children/method_contribution.rb create mode 100644 app/domain/models/contributions/mappers/method_contribution_mapper.rb create mode 100644 app/presentation/representers/complexity_representer.rb create mode 100644 app/presentation/representers/idiomaticity_representer.rb create mode 100644 app/presentation/representers/method_complexity_representer.rb create mode 100644 app/presentation/representers/method_contributions_representer.rb create mode 100644 spec/tests_integration/measurement_integration/productivity_measurement.rb diff --git a/Gemfile b/Gemfile index 4340db7..5934b4f 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,9 @@ gem 'aws-sdk-sqs', '~> 1' gem 'hirb', '~> 0.7' gem 'sequel', '~> 5.13' +# Ruby AST parser +gem 'unparser' + group :development, :test do gem 'database_cleaner' gem 'sqlite3' diff --git a/Gemfile.lock b/Gemfile.lock index 01edf4a..15425fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,15 @@ GEM remote: https://rubygems.org/ specs: + abstract_type (0.0.7) activesupport (5.2.2.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + adamantium (0.2.0) + ice_nine (~> 0.11.0) + memoizable (~> 0.4.0) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) ast (2.4.0) @@ -32,6 +36,9 @@ GEM coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) + concord (0.1.5) + adamantium (~> 0.2.0) + equalizer (~> 0.0.9) concurrent-ruby (1.1.3) connection_pool (2.2.2) cookiejar (0.3.3) @@ -43,6 +50,7 @@ GEM declarative-option (0.1.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.3) docile (1.3.1) domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) @@ -160,6 +168,8 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) + memoizable (0.4.2) + thread_safe (~> 0.3, >= 0.3.1) method_source (0.9.2) minitest (5.11.3) minitest-rg (5.2.0) @@ -175,6 +185,7 @@ GEM path_expander (1.0.3) pg (0.21.0) powerpack (0.1.2) + procto (0.0.3) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -265,6 +276,14 @@ GEM unf_ext unf_ext (0.0.7.5) unicode-display_width (1.4.0) + unparser (0.4.2) + abstract_type (~> 0.0.7) + adamantium (~> 0.2.0) + concord (~> 0.1.5) + diff-lcs (~> 1.3) + equalizer (~> 0.0.9) + parser (>= 2.3.1.2, < 2.6) + procto (~> 0.0.2) vcr (4.0.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -320,6 +339,7 @@ DEPENDENCIES simplecov (~> 0.16) sqlite3 travis + unparser vcr (~> 4.0) webmock (~> 3.4) diff --git a/app/domain/models/contributions/entities/children/file_contributions.rb b/app/domain/models/contributions/entities/children/file_contributions.rb index 8570560..88c6ab8 100644 --- a/app/domain/models/contributions/entities/children/file_contributions.rb +++ b/app/domain/models/contributions/entities/children/file_contributions.rb @@ -11,13 +11,14 @@ class FileContributions WANTED_EXTENSION = %w[rb js css html slim md].join('|') EXTENSION_REGEX = /#{DOT}#{WANTED_EXTENSION}#{LINE_END}/.freeze - attr_reader :file_path, :lines, :complexity, :idiomaticity + attr_reader :file_path, :lines, :complexity, :idiomaticity, :methods - def initialize(file_path:, lines:, complexity:, idiomaticity:) + def initialize(file_path:, lines:, complexity:, idiomaticity:, methods:) @file_path = Value::FilePath.new(file_path) @lines = lines @complexity = complexity @idiomaticity = idiomaticity + @methods = methods end def total_credits diff --git a/app/domain/models/contributions/entities/children/method_contribution.rb b/app/domain/models/contributions/entities/children/method_contribution.rb new file mode 100644 index 0000000..4447b82 --- /dev/null +++ b/app/domain/models/contributions/entities/children/method_contribution.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative 'line_contribution' +require 'dry-types' +require 'dry-struct' + +module CodePraise + module Entity + # Entity for a single method contributed by a team-member + class MethodContribution < Dry::Struct + + include Dry::Types.module + + attribute :name, Coercible::String + attribute :lines, Array.of(LineContribution) + + def credit_share + @credit_share ||= lines + .each_with_object(Value::CreditShare.new) do |line, credit| + credit.add_credit(line) + end + end + + def contributors + credit_share.contributors + end + end + end +end diff --git a/app/domain/models/contributions/entities/metrics/complexity.rb b/app/domain/models/contributions/entities/metrics/complexity.rb index 852fec5..f3cfee4 100644 --- a/app/domain/models/contributions/entities/metrics/complexity.rb +++ b/app/domain/models/contributions/entities/metrics/complexity.rb @@ -14,13 +14,16 @@ def initialize(file_path:) end def average + return nil if @flog.nil? @flog.average end - def methods_score - methods_score = @flog.totals - methods_score.keys.inject({}) do |result, file_path| - result[method_name(file_path)] = @flog.totals[file_path] + def methods_complexity + return nil if @flog.nil? + @flog.totals.keys.inject([]) do |result, file_path| + result.push( + OpenStruct.new(name: method_name(file_path), complexity: @flog.totals[file_path]) + ) result end end diff --git a/app/domain/models/contributions/entities/metrics/idiomaticity.rb b/app/domain/models/contributions/entities/metrics/idiomaticity.rb index e128fd2..126d128 100644 --- a/app/domain/models/contributions/entities/metrics/idiomaticity.rb +++ b/app/domain/models/contributions/entities/metrics/idiomaticity.rb @@ -17,11 +17,13 @@ def initialize(file_path:) @result = team.inspect_file(process_source) end - def count + def error_count + return nil if @result.nil? @result.count end - def messages + def error_messages + return nil if @result.nil? @result.map(&:message) end diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index f2aa58f..84cc71e 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -66,8 +66,30 @@ def contributors credit_share.contributors end + def subfolder_contributions + result = credit_share.contributors.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} + subfolders.each do |subfolder| + result.each do |k, v| + percentage = subfolder.credit_share.percentage + result[k].push({percentage: (percentage[k].nil? ? 0.0 : percentage[k]), path: subfolder.path}) + end + end + result + end + private + def variance(x) + m = mean(x) + sum = 0.0 + x.each {|v| sum += (v-m)**2 } + (sum/x.size).round(3) + end + + def mean(x) + x.reduce(:+).to_f / x.count + end + def comparitive_path path.empty? ? path : path + '/' end diff --git a/app/domain/models/contributions/lib/contributions_calculator.rb b/app/domain/models/contributions/lib/contributions_calculator.rb index 1f43316..95ac4ec 100644 --- a/app/domain/models/contributions/lib/contributions_calculator.rb +++ b/app/domain/models/contributions/lib/contributions_calculator.rb @@ -23,6 +23,10 @@ def credits_for(contributor) def percent_credit_of(contributor) ((credits_for(contributor).to_f / line_count) * 100).round end + + def total_methods + @methods.count + end end end end diff --git a/app/domain/models/contributions/mappers/file_contributions_mapper.rb b/app/domain/models/contributions/mappers/file_contributions_mapper.rb index 20b7f34..d273692 100644 --- a/app/domain/models/contributions/mappers/file_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/file_contributions_mapper.rb @@ -14,7 +14,8 @@ def build_entity file_path: filename, lines: contributions, complexity: complexity, - idiomaticity: idiomaticity + idiomaticity: idiomaticity, + methods: methods ) end @@ -49,6 +50,17 @@ def summarize_line_reports(line_reports) end end + def methods + return [] unless ruby_file? + MethodContributions.new( + file_code: contributions + ).build_entity + end + + def ruby_file? + File.extname(@file_report[0]) == ".rb" + end + def contributor_from(report) Entity::Contributor.new( username: report['author'], diff --git a/app/domain/models/contributions/mappers/method_contribution_mapper.rb b/app/domain/models/contributions/mappers/method_contribution_mapper.rb new file mode 100644 index 0000000..fec3ece --- /dev/null +++ b/app/domain/models/contributions/mappers/method_contribution_mapper.rb @@ -0,0 +1,75 @@ +require 'parser/current' +require 'unparser' + + +module CodePraise + + module Mapper + + class MethodContributions + def initialize(file_code) + @file_code = file_code[:file_code] + @codes = @file_code.map(&:code).join("\n") + @ast = Parser::CurrentRuby.parse(@codes) + end + + def build_entity + all_methods.map do |method| + Entity::MethodContribution.new( + name: method[:name], + lines: method[:lines] + ) + end + end + + private + + def all_methods + methods = [] + find_methods(@ast, methods) + return [] if methods.empty? + methods.inject([]) do |result, method| + result.push({ + name: method_name(method), + lines: line_contribution(method) + }) + end + end + + def line_contribution(method) + method_codes = Unparser.unparse(method).split("\n") + first_no = @file_code.select {|loc| loc.code.strip == method_codes[0].strip}[0].number + end_no = first_no + method_codes.count - 1 + result = [] + while first_no <= end_no + loc = @file_code.select {|loc| loc.number == first_no}[0] + first_no +=1 + result.push(loc) + break if loc.nil? + end_no += 1 if loc.code.strip.empty? + end + result + end + + + def method_name(ast) + if ast.type == :block + return ast.children[0].children[1] + elsif ast.type == :def + return ast.children[0] + end + end + + def find_methods(ast, methods) + return unless Parser::AST::Node === ast + if ast.type == :def + methods.append(ast) + else + ast.children.each do |ast| + find_methods(ast, methods) + end + end + end + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/repositories/git_repo.rb b/app/domain/models/contributions/repositories/git_repo.rb index 84ad1a1..43ed79f 100644 --- a/app/domain/models/contributions/repositories/git_repo.rb +++ b/app/domain/models/contributions/repositories/git_repo.rb @@ -3,7 +3,7 @@ module CodePraise # Maps over local and remote git repo infrastructure class GitRepo - MAX_SIZE = 1000 # for cloning, analysis, summaries, etc. + MAX_SIZE = 100000 # for cloning, analysis, summaries, etc. class Errors NoGitRepoFound = Class.new(StandardError) diff --git a/app/domain/models/contributions/values/credit_share.rb b/app/domain/models/contributions/values/credit_share.rb index f76a1dc..1950733 100644 --- a/app/domain/models/contributions/values/credit_share.rb +++ b/app/domain/models/contributions/values/credit_share.rb @@ -30,6 +30,15 @@ def ==(other) other.class == self.class && other.state == self.state end + def percentage + sum = share.values.reduce(:+) + result = {} + share.each do |k, v| + result[k] = (v.to_f / sum).round(2) + end + result + end + alias eql? == def hash diff --git a/app/presentation/representers/complexity_representer.rb b/app/presentation/representers/complexity_representer.rb new file mode 100644 index 0000000..4db9e35 --- /dev/null +++ b/app/presentation/representers/complexity_representer.rb @@ -0,0 +1,15 @@ +require 'roar/decorator' +require 'roar/json' +require_relative 'method_complexity_representer' + +module CodePraise + module Representer + # Represents folder summary about repo's folder + class Complexity < Roar::Decorator + include Roar::JSON + + property :average + collection :methods_complexity, extend: Representer::MethodComplexityRepresenter, class: OpenStruct + end + end +end diff --git a/app/presentation/representers/file_contributions_representer.rb b/app/presentation/representers/file_contributions_representer.rb index 6ca692d..55a169d 100644 --- a/app/presentation/representers/file_contributions_representer.rb +++ b/app/presentation/representers/file_contributions_representer.rb @@ -7,6 +7,9 @@ require_relative 'credit_share_representer' require_relative 'file_path_representer' require_relative 'line_contribution_representer' +require_relative 'complexity_representer' +require_relative 'idiomaticity_representer' +require_relative 'method_contributions_representer' module CodePraise module Representer @@ -16,8 +19,12 @@ class FileContributions < Roar::Decorator property :line_count property :total_credits + property :total_methods + collection :methods, extend: Representer::MethodContributions, class: OpenStruct property :file_path, extend: Representer::FilePath, class: OpenStruct property :credit_share, extend: Representer::CreditShare, class: OpenStruct + property :complexity, extend: Representer::Complexity, class: OpenStruct + property :idiomaticity, extend: Representer::Idiomaticity, class: OpenStruct collection :contributors, extend: Representer::Contributor, class: OpenStruct end end diff --git a/app/presentation/representers/idiomaticity_representer.rb b/app/presentation/representers/idiomaticity_representer.rb new file mode 100644 index 0000000..8f191be --- /dev/null +++ b/app/presentation/representers/idiomaticity_representer.rb @@ -0,0 +1,14 @@ +require 'roar/decorator' +require 'roar/json' + +module CodePraise + module Representer + # Represents folder summary about repo's folder + class Idiomaticity < Roar::Decorator + include Roar::JSON + + property :error_count + collection :error_messages + end + end +end diff --git a/app/presentation/representers/method_complexity_representer.rb b/app/presentation/representers/method_complexity_representer.rb new file mode 100644 index 0000000..35bcc76 --- /dev/null +++ b/app/presentation/representers/method_complexity_representer.rb @@ -0,0 +1,14 @@ +require 'roar/decorator' +require 'roar/json' + +module CodePraise + module Representer + # Represents folder summary about repo's folder + class MethodComplexityRepresenter < Roar::Decorator + include Roar::JSON + + property :name + property :complexity + end + end +end diff --git a/app/presentation/representers/method_contributions_representer.rb b/app/presentation/representers/method_contributions_representer.rb new file mode 100644 index 0000000..232d525 --- /dev/null +++ b/app/presentation/representers/method_contributions_representer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'roar/decorator' +require 'roar/json' + +require_relative 'contributor_representer' +require_relative 'credit_share_representer' + +module CodePraise + module Representer + # Represents folder summary about repo's folder + class MethodContributions < Roar::Decorator + include Roar::JSON + + property :name + property :credit_share, extend: Representer::CreditShare, class: OpenStruct + end + end +end diff --git a/spec/tests_integration/measurement_integration/productivity_measurement.rb b/spec/tests_integration/measurement_integration/productivity_measurement.rb new file mode 100644 index 0000000..2491db2 --- /dev/null +++ b/spec/tests_integration/measurement_integration/productivity_measurement.rb @@ -0,0 +1,20 @@ +require_relative '../../helpers/spec_helper.rb' + +describe "Test Productivtiy Measurement" do + + before do + project = create(:project) + git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) + folder = CodePraise::Mapper::Contributions.new(git_repo).for_folder('app') + @file = folder.files[0] + binding.pry + end + + describe "Entity::MethodContribution" do + it "should return number of method" do + methods = @file.methods + _(methods.count).wont_be_nil + _(methods[0].lines).wont_be_nil + end + end +end \ No newline at end of file diff --git a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb b/spec/tests_integration/measurement_integration/quality_measurement_spec.rb index a14c640..b73a15b 100644 --- a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb +++ b/spec/tests_integration/measurement_integration/quality_measurement_spec.rb @@ -12,7 +12,7 @@ describe "Entity::Complexity" do it "should return complexity score" do _(@file.complexity.average).wont_be_nil - _(@file.complexity.methods_score.keys).wont_be_empty + _(@file.complexity.methods_complexity.keys).wont_be_empty end end From 3b0143baaa0da7cbdca255d1cea1d2d4401f142e Mon Sep 17 00:00:00 2001 From: vicxu Date: Mon, 25 Mar 2019 22:06:33 +0800 Subject: [PATCH 05/14] create measurement module for quality metrics --- .../entities/children/file_contributions.rb | 5 +- .../entities/children/method_contribution.rb | 11 ++- .../entities/metrics/collective_ownership.rb | 16 ++++ .../entities/metrics/complexity.rb | 33 +------- .../entities/metrics/idiomaticity.rb | 36 ++------- .../entities/root/folder_contributions.rb | 23 +++--- .../lib/measurement/collective_ownership.rb | 41 ++++++++++ .../lib/measurement/complexity.rb | 41 ++++++++++ .../lib/measurement/idiomaticity.rb | 38 +++++++++ .../lib/measurement/number_of_annotation.rb | 34 ++++++++ .../lib/measurement/number_of_method.rb | 77 +++++++++++++++++++ .../mappers/file_contributions_mapper.rb | 27 +++++-- .../mappers/method_contribution_mapper.rb | 55 +------------ .../contributions/values/credit_share.rb | 12 +-- ...rb => collective_ownership_representer.rb} | 8 +- .../representers/complexity_representer.rb | 3 +- .../file_contributions_representer.rb | 1 + .../folder_contributions_representer.rb | 2 + .../method_contributions_representer.rb | 2 +- init.rb | 2 +- lib/init.rb | 5 ++ lib/math_extension.rb | 22 ++++++ .../quality_measurement_spec.rb | 15 +++- 23 files changed, 360 insertions(+), 149 deletions(-) create mode 100644 app/domain/models/contributions/entities/metrics/collective_ownership.rb create mode 100644 app/domain/models/contributions/lib/measurement/collective_ownership.rb create mode 100644 app/domain/models/contributions/lib/measurement/complexity.rb create mode 100644 app/domain/models/contributions/lib/measurement/idiomaticity.rb create mode 100644 app/domain/models/contributions/lib/measurement/number_of_annotation.rb create mode 100644 app/domain/models/contributions/lib/measurement/number_of_method.rb rename app/presentation/representers/{method_complexity_representer.rb => collective_ownership_representer.rb} (55%) create mode 100644 lib/init.rb create mode 100644 lib/math_extension.rb diff --git a/app/domain/models/contributions/entities/children/file_contributions.rb b/app/domain/models/contributions/entities/children/file_contributions.rb index 88c6ab8..3020613 100644 --- a/app/domain/models/contributions/entities/children/file_contributions.rb +++ b/app/domain/models/contributions/entities/children/file_contributions.rb @@ -11,14 +11,15 @@ class FileContributions WANTED_EXTENSION = %w[rb js css html slim md].join('|') EXTENSION_REGEX = /#{DOT}#{WANTED_EXTENSION}#{LINE_END}/.freeze - attr_reader :file_path, :lines, :complexity, :idiomaticity, :methods + attr_reader :file_path, :lines, :complexity, :idiomaticity, :methods, :total_annotations - def initialize(file_path:, lines:, complexity:, idiomaticity:, methods:) + def initialize(file_path:, lines:, complexity:, idiomaticity:, methods:, total_annotations:) @file_path = Value::FilePath.new(file_path) @lines = lines @complexity = complexity @idiomaticity = idiomaticity @methods = methods + @total_annotations = total_annotations end def total_credits diff --git a/app/domain/models/contributions/entities/children/method_contribution.rb b/app/domain/models/contributions/entities/children/method_contribution.rb index 4447b82..5053ef2 100644 --- a/app/domain/models/contributions/entities/children/method_contribution.rb +++ b/app/domain/models/contributions/entities/children/method_contribution.rb @@ -15,10 +15,13 @@ class MethodContribution < Dry::Struct attribute :lines, Array.of(LineContribution) def credit_share - @credit_share ||= lines - .each_with_object(Value::CreditShare.new) do |line, credit| - credit.add_credit(line) - end + @credit_share ||= lines.each_with_object(Value::CreditShare.new) do |line, credit| + credit.add_credit(line) + end + end + + def share + credit_share.share end def contributors diff --git a/app/domain/models/contributions/entities/metrics/collective_ownership.rb b/app/domain/models/contributions/entities/metrics/collective_ownership.rb new file mode 100644 index 0000000..9fa715a --- /dev/null +++ b/app/domain/models/contributions/entities/metrics/collective_ownership.rb @@ -0,0 +1,16 @@ + +module CodePraise + module Entity + + class CollectiveOwnership + + attr_reader :contributor, :coefficient_variation, :contributions + + def initialize(contributor:, coefficient_variation:, contributions:) + @contributor = contributor + @coefficient_variation = coefficient_variation + @contributions = contributions + end + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/entities/metrics/complexity.rb b/app/domain/models/contributions/entities/metrics/complexity.rb index f3cfee4..4535e24 100644 --- a/app/domain/models/contributions/entities/metrics/complexity.rb +++ b/app/domain/models/contributions/entities/metrics/complexity.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require 'flog' module CodePraise @@ -7,35 +6,11 @@ module Entity class Complexity - def initialize(file_path:) - return nil unless ruby_file?(file_path) - @flog = Flog.new - @flog.flog(*file_path) - end - - def average - return nil if @flog.nil? - @flog.average - end - - def methods_complexity - return nil if @flog.nil? - @flog.totals.keys.inject([]) do |result, file_path| - result.push( - OpenStruct.new(name: method_name(file_path), complexity: @flog.totals[file_path]) - ) - result - end - end - - private - - def ruby_file?(file_path) - File.extname(file_path) == '.rb' - end + attr_reader :average, :methods_complexity - def method_name(file_path) - file_path.split("#").last + def initialize(average: ,methods_complexity: ) + @average = average + @methods_complexity = methods_complexity end end end diff --git a/app/domain/models/contributions/entities/metrics/idiomaticity.rb b/app/domain/models/contributions/entities/metrics/idiomaticity.rb index 126d128..ddc69bf 100644 --- a/app/domain/models/contributions/entities/metrics/idiomaticity.rb +++ b/app/domain/models/contributions/entities/metrics/idiomaticity.rb @@ -1,41 +1,19 @@ # frozen_string_literal: true -require 'rubocop' + +require 'dry-types' +require 'dry-struct' module CodePraise module Entity - class Idiomaticity - - def initialize(file_path:) - return nil unless ruby_file?(file_path) - process_source = RuboCop::ProcessedSource.from_file(file_path, ruby_version) - config = RuboCop::ConfigStore.new.for(process_source.path) - cop_classes = RuboCop::Cop::Cop.all - registry = RuboCop::Cop::Registry.new(cop_classes) - team = RuboCop::Cop::Team.new(registry, config) - @result = team.inspect_file(process_source) - end - - def error_count - return nil if @result.nil? - @result.count - end - - def error_messages - return nil if @result.nil? - @result.map(&:message) - end + class Idiomaticity < Dry::Struct - private + include Dry::Types.module - def ruby_file?(file_path) - File.extname(file_path) == '.rb' - end + attribute :error_count, Strict::Integer.optional + attribute :error_messages, Strict::Array.of(Strict::String).optional - def ruby_version - RUBY_VERSION.to_f - end end end end diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index 84cc71e..4b32552 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require_relative '../../lib/measurement/collective_ownership' module CodePraise module Entity @@ -18,6 +19,17 @@ def initialize(path:, files:, repo_path:) subfolders.each { |folder| self[folder.path] = folder } end + def collective_ownership + return nil unless any_subfolders? || credit_share == 0 + Measurement::CollectiveOwnership.calculate(subfolders, contributors).map do |co| + Entity::CollectiveOwnership.new( + contributor: co[:contributor], + contributions: co[:contributions], + coefficient_variation: co[:coefficient_variation] + ) + end + end + def line_count files.map(&:line_count).reduce(&:+) end @@ -79,17 +91,6 @@ def subfolder_contributions private - def variance(x) - m = mean(x) - sum = 0.0 - x.each {|v| sum += (v-m)**2 } - (sum/x.size).round(3) - end - - def mean(x) - x.reduce(:+).to_f / x.count - end - def comparitive_path path.empty? ? path : path + '/' end diff --git a/app/domain/models/contributions/lib/measurement/collective_ownership.rb b/app/domain/models/contributions/lib/measurement/collective_ownership.rb new file mode 100644 index 0000000..26b7177 --- /dev/null +++ b/app/domain/models/contributions/lib/measurement/collective_ownership.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module CodePraise + + module Measurement + + module CollectiveOwnership + + def self.calculate(sub_folders, contributors) + subfolders_percentage_hash = subfolders_percentage(sub_folders, contributors) + subfolders_percentage_hash.keys.inject([]) do |result, key| + result.push( + { + contributor: key, + contributions: subfolders_percentage_hash[key], + coefficient_variation: Math.coefficient_variation(percentage_nums(subfolders_percentage_hash[key])) + } + ) + end + end + + private + + def self.subfolders_percentage(sub_folders, contributors) + result = contributors.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} + sub_folders.each do |subfolder| + result.each do |k, v| + share_percentage = subfolder.credit_share.share_percentage + break if share_percentage.nil? + result[k].push({percentage: (share_percentage[k].nil? ? 0 : share_percentage[k]), folder: subfolder.path}) + end + end + result + end + + def self.percentage_nums(percentage_array) + percentage_array.map{|percentage_hash| percentage_hash[:percentage]} + end + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/complexity.rb b/app/domain/models/contributions/lib/measurement/complexity.rb new file mode 100644 index 0000000..260ce4b --- /dev/null +++ b/app/domain/models/contributions/lib/measurement/complexity.rb @@ -0,0 +1,41 @@ +require 'flog' + +module CodePraise + + module Measurement + + module Complexity + + def self.calculate(file_path) + result = { + average: nil, + methods_complexity: nil + } + if ruby_file?(file_path) + flog = Flog.new + flog.flog(*file_path) + result[:average] = flog.average + result[:methods_complexity] = methods_complexity(flog.totals) + end + result + end + + private + + def self.methods_complexity(flog_totals) + flog_totals.keys.inject({}) do |result, key| + result[method_name(key)] = flog_totals[key] + result + end + end + + def self.method_name(file_path) + file_path.split("#").last + end + + def self.ruby_file?(file_path) + File.extname(file_path) == '.rb' + end + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/idiomaticity.rb b/app/domain/models/contributions/lib/measurement/idiomaticity.rb new file mode 100644 index 0000000..ffa37f5 --- /dev/null +++ b/app/domain/models/contributions/lib/measurement/idiomaticity.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +require 'rubocop' + +module CodePraise + + module Measurement + + module Idiomaticity + + def self.calculate(file_path) + result = { + error_count: nil, + error_messages: nil + } + if ruby_file?(file_path) + process_source = RuboCop::ProcessedSource.from_file(file_path, ruby_version) + config = RuboCop::ConfigStore.new.for(process_source.path) + cop_classes = RuboCop::Cop::Cop.all + registry = RuboCop::Cop::Registry.new(cop_classes) + team = RuboCop::Cop::Team.new(registry, config) + result[:error_count] = team.inspect_file(process_source).count + result[:error_messages] = team.inspect_file(process_source).map(&:message) + end + result + end + + private + + def self.ruby_version + RUBY_VERSION.to_f + end + + def self.ruby_file?(file_path) + File.extname(file_path) == '.rb' + end + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/number_of_annotation.rb b/app/domain/models/contributions/lib/measurement/number_of_annotation.rb new file mode 100644 index 0000000..a3ea12b --- /dev/null +++ b/app/domain/models/contributions/lib/measurement/number_of_annotation.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module CodePraise + + module Measurement + + module NumberOfAnnotation + + LINE_LIMITATION = 2 + COMMENT = "#" + + def self.calculate(lines) + comment_index, not_comment_index, annotation = [0, 0, 0] + lines.each_with_index do |loc, i| + if is_comment?(loc) + comment_index = i + else + annotation += 1 if (comment_index - not_comment_index) >= LINE_LIMITATION + not_comment_index = i + end + end + annotation += 1 if (comment_index - not_comment_index) >= LINE_LIMITATION + annotation + end + + private + + def self.is_comment?(loc) + loc.strip[0] == COMMENT + end + + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/number_of_method.rb b/app/domain/models/contributions/lib/measurement/number_of_method.rb new file mode 100644 index 0000000..3be136d --- /dev/null +++ b/app/domain/models/contributions/lib/measurement/number_of_method.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'parser/current' +require 'unparser' + + +module CodePraise + + module Measurement + + module NumberOfMethod + + def self.calculate(contributions) + ast = Parser::CurrentRuby.parse(code(contributions)) + all_methods(ast, contributions) + end + + private + + def self.code(contributions) + contributions.map(&:code).join("\n") + end + + + def self.all_methods(ast, contributions) + methods = [] + find_methods(ast, methods) + return [] if methods.empty? + methods.inject([]) do |result, method_ast| + result.push({ + name: method_name(method_ast), + lines: line_contribution(method_ast, contributions) + }) + end + end + + def self.line_contribution(method_ast, contributions) + method_code_array = Unparser.unparse(method_ast).split("\n") + first_number = contributions.select {|loc| loc.code.strip == method_code_array[0].strip}[0].number + end_number = first_number + method_code_array.count - 1 + result = [] + while first_number <= end_number + loc = contributions.select {|loc| loc.number == first_number}[0] + first_number +=1 + result.push(loc) + break if loc.nil? + end_number += 1 if new_line?(loc) || comment?(loc) + end + result + end + + def self.new_line?(loc) + loc.code.strip.empty? + end + + def self.comment?(loc) + loc.code.strip[0] == '#' + end + + def self.method_name(method_ast) + method_ast.children[0] + end + + def self.find_methods(ast, methods) + return nil unless Parser::AST::Node === ast + if ast.type == :def + methods.append(ast) + else + ast.children.each do |ast| + find_methods(ast, methods) + end + end + end + + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/mappers/file_contributions_mapper.rb b/app/domain/models/contributions/mappers/file_contributions_mapper.rb index d273692..602ba9b 100644 --- a/app/domain/models/contributions/mappers/file_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/file_contributions_mapper.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require_relative '../lib/measurement/complexity' +require_relative '../lib/measurement/idiomaticity' +require_relative '../lib/measurement/number_of_annotation' + module CodePraise module Mapper # Summarizes a single file's contributions by team members @@ -15,7 +19,8 @@ def build_entity lines: contributions, complexity: complexity, idiomaticity: idiomaticity, - methods: methods + methods: methods, + total_annotations: total_annotations ) end @@ -31,12 +36,24 @@ def contributions def complexity file_path = Value::FilePath.new(filename) - Entity::Complexity.new(file_path: "#{@repo_path}/#{file_path}") + complexity_hash = Measurement::Complexity.calculate("#{@repo_path}/#{file_path}") + Entity::Complexity.new( + average: complexity_hash[:average], + methods_complexity: complexity_hash[:methods_complexity] + ) end def idiomaticity file_path = Value::FilePath.new(filename) - Entity::Idiomaticity.new(file_path: "#{@repo_path}/#{file_path}") + idiomaticity_hash = Measurement::Idiomaticity.calculate("#{@repo_path}/#{file_path}") + Entity::Idiomaticity.new( + error_count: idiomaticity_hash[:error_count], + error_messages: idiomaticity_hash[:error_messages] + ) + end + + def total_annotations + Measurement::NumberOfAnnotation.calculate(contributions.map(&:code)) end def summarize_line_reports(line_reports) @@ -52,9 +69,7 @@ def summarize_line_reports(line_reports) def methods return [] unless ruby_file? - MethodContributions.new( - file_code: contributions - ).build_entity + MethodContributions.new(contributions).build_entity end def ruby_file? diff --git a/app/domain/models/contributions/mappers/method_contribution_mapper.rb b/app/domain/models/contributions/mappers/method_contribution_mapper.rb index fec3ece..cb28358 100644 --- a/app/domain/models/contributions/mappers/method_contribution_mapper.rb +++ b/app/domain/models/contributions/mappers/method_contribution_mapper.rb @@ -1,16 +1,12 @@ -require 'parser/current' -require 'unparser' - +require_relative '../lib/measurement/number_of_method' module CodePraise module Mapper class MethodContributions - def initialize(file_code) - @file_code = file_code[:file_code] - @codes = @file_code.map(&:code).join("\n") - @ast = Parser::CurrentRuby.parse(@codes) + def initialize(file_contributions) + @file_contributions = file_contributions end def build_entity @@ -25,50 +21,7 @@ def build_entity private def all_methods - methods = [] - find_methods(@ast, methods) - return [] if methods.empty? - methods.inject([]) do |result, method| - result.push({ - name: method_name(method), - lines: line_contribution(method) - }) - end - end - - def line_contribution(method) - method_codes = Unparser.unparse(method).split("\n") - first_no = @file_code.select {|loc| loc.code.strip == method_codes[0].strip}[0].number - end_no = first_no + method_codes.count - 1 - result = [] - while first_no <= end_no - loc = @file_code.select {|loc| loc.number == first_no}[0] - first_no +=1 - result.push(loc) - break if loc.nil? - end_no += 1 if loc.code.strip.empty? - end - result - end - - - def method_name(ast) - if ast.type == :block - return ast.children[0].children[1] - elsif ast.type == :def - return ast.children[0] - end - end - - def find_methods(ast, methods) - return unless Parser::AST::Node === ast - if ast.type == :def - methods.append(ast) - else - ast.children.each do |ast| - find_methods(ast, methods) - end - end + Measurement::NumberOfMethod.calculate(@file_contributions) end end end diff --git a/app/domain/models/contributions/values/credit_share.rb b/app/domain/models/contributions/values/credit_share.rb index 1950733..ea79cb7 100644 --- a/app/domain/models/contributions/values/credit_share.rb +++ b/app/domain/models/contributions/values/credit_share.rb @@ -30,13 +30,13 @@ def ==(other) other.class == self.class && other.state == self.state end - def percentage - sum = share.values.reduce(:+) - result = {} - share.each do |k, v| - result[k] = (v.to_f / sum).round(2) + def share_percentage + sum = share.values.reduce(&:+) + return nil if sum.nil? || sum == 0 + share.keys.inject({}) do |result, key| + result[key] = ((share[key].to_f / sum) * 100).round + result end - result end alias eql? == diff --git a/app/presentation/representers/method_complexity_representer.rb b/app/presentation/representers/collective_ownership_representer.rb similarity index 55% rename from app/presentation/representers/method_complexity_representer.rb rename to app/presentation/representers/collective_ownership_representer.rb index 35bcc76..0f56e6b 100644 --- a/app/presentation/representers/method_complexity_representer.rb +++ b/app/presentation/representers/collective_ownership_representer.rb @@ -1,14 +1,16 @@ require 'roar/decorator' require 'roar/json' + module CodePraise module Representer # Represents folder summary about repo's folder - class MethodComplexityRepresenter < Roar::Decorator + class CollectiveOwnership < Roar::Decorator include Roar::JSON - property :name - property :complexity + property :contributor + collection :contributions + property :coefficient_variation end end end diff --git a/app/presentation/representers/complexity_representer.rb b/app/presentation/representers/complexity_representer.rb index 4db9e35..bec97c3 100644 --- a/app/presentation/representers/complexity_representer.rb +++ b/app/presentation/representers/complexity_representer.rb @@ -1,6 +1,5 @@ require 'roar/decorator' require 'roar/json' -require_relative 'method_complexity_representer' module CodePraise module Representer @@ -9,7 +8,7 @@ class Complexity < Roar::Decorator include Roar::JSON property :average - collection :methods_complexity, extend: Representer::MethodComplexityRepresenter, class: OpenStruct + property :methods_complexity end end end diff --git a/app/presentation/representers/file_contributions_representer.rb b/app/presentation/representers/file_contributions_representer.rb index 55a169d..888acb7 100644 --- a/app/presentation/representers/file_contributions_representer.rb +++ b/app/presentation/representers/file_contributions_representer.rb @@ -20,6 +20,7 @@ class FileContributions < Roar::Decorator property :line_count property :total_credits property :total_methods + property :total_annotations collection :methods, extend: Representer::MethodContributions, class: OpenStruct property :file_path, extend: Representer::FilePath, class: OpenStruct property :credit_share, extend: Representer::CreditShare, class: OpenStruct diff --git a/app/presentation/representers/folder_contributions_representer.rb b/app/presentation/representers/folder_contributions_representer.rb index ee31f69..1c5dd57 100644 --- a/app/presentation/representers/folder_contributions_representer.rb +++ b/app/presentation/representers/folder_contributions_representer.rb @@ -7,6 +7,7 @@ require_relative 'credit_share_representer' require_relative 'file_contributions_representer' require_relative 'line_contribution_representer' +require_relative 'collective_ownership_representer' module CodePraise module Representer @@ -19,6 +20,7 @@ class FolderContributions < Roar::Decorator property :total_credits property :any_subfolders? property :any_base_files? + collection :collective_ownership, extend: Representer::CollectiveOwnership, class: OpenStruct property :credit_share, extend: Representer::CreditShare, class: OpenStruct collection :base_files, extend: Representer::FileContributions, class: OpenStruct collection :subfolders, extend: Representer::FolderContributions, class: OpenStruct diff --git a/app/presentation/representers/method_contributions_representer.rb b/app/presentation/representers/method_contributions_representer.rb index 232d525..d58feae 100644 --- a/app/presentation/representers/method_contributions_representer.rb +++ b/app/presentation/representers/method_contributions_representer.rb @@ -13,7 +13,7 @@ class MethodContributions < Roar::Decorator include Roar::JSON property :name - property :credit_share, extend: Representer::CreditShare, class: OpenStruct + property :share end end end diff --git a/init.rb b/init.rb index a8a4f09..cbe4bc1 100644 --- a/init.rb +++ b/init.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -folders = %w[config app] +folders = %w[lib config app] folders.each do |folder| require_relative "#{folder}/init.rb" end diff --git a/lib/init.rb b/lib/init.rb new file mode 100644 index 0000000..0019f37 --- /dev/null +++ b/lib/init.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Dir.glob("#{__dir__}/*.rb").each do |file| + require file +end diff --git a/lib/math_extension.rb b/lib/math_extension.rb new file mode 100644 index 0000000..609311e --- /dev/null +++ b/lib/math_extension.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Math + def self.coefficient_variation(nums) + (sigma(nums) / mean(nums) * 100).round + end + + def self.sigma(nums) + Math.sqrt(variance(nums)) + end + + def self.variance(nums) + m = mean(nums) + sum = 0.0 + nums.each {|v| sum += (v-m)**2 } + (sum/nums.size).round(3) + end + + def self.mean(nums) + nums.reduce(:+).to_f / nums.count + end +end \ No newline at end of file diff --git a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb b/spec/tests_integration/measurement_integration/quality_measurement_spec.rb index b73a15b..055eea6 100644 --- a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb +++ b/spec/tests_integration/measurement_integration/quality_measurement_spec.rb @@ -5,8 +5,10 @@ before do project = create(:project) git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) - folder = CodePraise::Mapper::Contributions.new(git_repo).for_folder('app') + contributions = CodePraise::Mapper::Contributions.new(git_repo) + folder = contributions.for_folder('app') @file = folder.files[0] + binding.pry end describe "Entity::Complexity" do @@ -18,9 +20,14 @@ describe "Entity::Idiomaticity" do it "should count number of unidiomatic code" do - _(@file.idiomaticity.count).wont_be_nil - _(@file.idiomaticity.messages).wont_be_empty - binding.pry + _(@file.idiomaticity.error_count).wont_be_nil + _(@file.idiomaticity.error_messages).wont_be_empty + end + end + + describe "Annotation" do + it "should return number of annotation" do + _(@file.total_annotations).wont_be_nil end end end \ No newline at end of file From daf282840808eeb4c50bfa423828fe3afe4bc3a8 Mon Sep 17 00:00:00 2001 From: vicxu Date: Mon, 25 Mar 2019 22:19:13 +0800 Subject: [PATCH 06/14] change new_line to blank_line in NumberOfMethod module --- .../models/contributions/lib/measurement/number_of_method.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/domain/models/contributions/lib/measurement/number_of_method.rb b/app/domain/models/contributions/lib/measurement/number_of_method.rb index 3be136d..f991c25 100644 --- a/app/domain/models/contributions/lib/measurement/number_of_method.rb +++ b/app/domain/models/contributions/lib/measurement/number_of_method.rb @@ -44,12 +44,12 @@ def self.line_contribution(method_ast, contributions) first_number +=1 result.push(loc) break if loc.nil? - end_number += 1 if new_line?(loc) || comment?(loc) + end_number += 1 if blank_line?(loc) || comment?(loc) end result end - def self.new_line?(loc) + def self.blank_line?(loc) loc.code.strip.empty? end From 630b169d74985ae41e13ae3f5e72e664f131ac04 Mon Sep 17 00:00:00 2001 From: vicxu Date: Tue, 26 Mar 2019 13:32:41 +0800 Subject: [PATCH 07/14] refactor method name for all module in measurement module --- Gemfile | 5 ++- Gemfile.lock | 2 + .../entities/metrics/collective_ownership.rb | 1 + .../lib/measurement/collective_ownership.rb | 26 +++++++----- .../lib/measurement/complexity.rb | 17 +++++--- .../lib/measurement/idiomaticity.rb | 19 +++++---- .../lib/measurement/number_of_annotation.rb | 8 +++- .../lib/measurement/number_of_method.rb | 40 +++++++++---------- ...ent_spec.rb => file_level_metrics_spec.rb} | 22 ++++++++-- .../productivity_measurement.rb | 20 ---------- 10 files changed, 90 insertions(+), 70 deletions(-) rename spec/tests_integration/measurement_integration/{quality_measurement_spec.rb => file_level_metrics_spec.rb} (62%) delete mode 100644 spec/tests_integration/measurement_integration/productivity_measurement.rb diff --git a/Gemfile b/Gemfile index 5934b4f..0331ea8 100644 --- a/Gemfile +++ b/Gemfile @@ -37,9 +37,12 @@ gem 'aws-sdk-sqs', '~> 1' gem 'hirb', '~> 0.7' gem 'sequel', '~> 5.13' -# Ruby AST parser +# Ruby AST unparser gem 'unparser' +# Git Operation by using git object +gem 'git', '~> 1.5' + group :development, :test do gem 'database_cleaner' gem 'sqlite3' diff --git a/Gemfile.lock b/Gemfile.lock index 15425fe..a68c196 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,6 +142,7 @@ GEM multi_json (~> 1.0) net-http-persistent (>= 2.7) net-http-pipeline + git (1.5.0) hashdiff (0.3.7) highline (1.7.10) hirb (0.7.3) @@ -314,6 +315,7 @@ DEPENDENCIES factory_bot (~> 5.0, >= 5.0.2) faye (~> 1) flog + git (~> 1.5) hirb (~> 0.7) http (~> 3.0) minitest (~> 5.11) diff --git a/app/domain/models/contributions/entities/metrics/collective_ownership.rb b/app/domain/models/contributions/entities/metrics/collective_ownership.rb index 9fa715a..d17f8cd 100644 --- a/app/domain/models/contributions/entities/metrics/collective_ownership.rb +++ b/app/domain/models/contributions/entities/metrics/collective_ownership.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module CodePraise module Entity diff --git a/app/domain/models/contributions/lib/measurement/collective_ownership.rb b/app/domain/models/contributions/lib/measurement/collective_ownership.rb index 26b7177..eb610ea 100644 --- a/app/domain/models/contributions/lib/measurement/collective_ownership.rb +++ b/app/domain/models/contributions/lib/measurement/collective_ownership.rb @@ -6,14 +6,14 @@ module Measurement module CollectiveOwnership - def self.calculate(sub_folders, contributors) - subfolders_percentage_hash = subfolders_percentage(sub_folders, contributors) - subfolders_percentage_hash.keys.inject([]) do |result, key| + def self.calculate(sub_folders, contributor_entities) + subfolders_percentage_hash = subfolders_percentage(sub_folders, contributor_entities) + subfolders_percentage_hash.keys.inject([]) do |result, name| result.push( { - contributor: key, - contributions: subfolders_percentage_hash[key], - coefficient_variation: Math.coefficient_variation(percentage_nums(subfolders_percentage_hash[key])) + contributor: name, + contributions: subfolders_percentage_hash[name], + coefficient_variation: Math.coefficient_variation(percentage_nums(subfolders_percentage_hash[name])) } ) end @@ -21,16 +21,20 @@ def self.calculate(sub_folders, contributors) private - def self.subfolders_percentage(sub_folders, contributors) - result = contributors.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} + def self.subfolders_percentage(sub_folders, contributor_entities) + contributors = contributor_empty_hash(contributor_entities) sub_folders.each do |subfolder| - result.each do |k, v| + contributors.each do |name, v| share_percentage = subfolder.credit_share.share_percentage break if share_percentage.nil? - result[k].push({percentage: (share_percentage[k].nil? ? 0 : share_percentage[k]), folder: subfolder.path}) + contributors[name].push({percentage: (share_percentage[name].nil? ? 0 : share_percentage[name]), folder: subfolder.path}) end end - result + contributors + end + + def self.contributor_empty_hash(contributor_entities) + contributor_entities.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} end def self.percentage_nums(percentage_array) diff --git a/app/domain/models/contributions/lib/measurement/complexity.rb b/app/domain/models/contributions/lib/measurement/complexity.rb index 260ce4b..169c180 100644 --- a/app/domain/models/contributions/lib/measurement/complexity.rb +++ b/app/domain/models/contributions/lib/measurement/complexity.rb @@ -12,24 +12,29 @@ def self.calculate(file_path) methods_complexity: nil } if ruby_file?(file_path) - flog = Flog.new - flog.flog(*file_path) - result[:average] = flog.average - result[:methods_complexity] = methods_complexity(flog.totals) + flog_result = flog_process(file_path) + result[:average] = flog_result.average + result[:methods_complexity] = methods_complexity(flog_result.totals) end result end private + def self.flog_process(file_path) + flog = Flog.new + flog.flog(*file_path) + flog + end + def self.methods_complexity(flog_totals) flog_totals.keys.inject({}) do |result, key| - result[method_name(key)] = flog_totals[key] + result[only_method_name(key)] = flog_totals[key] result end end - def self.method_name(file_path) + def self.only_method_name(file_path) file_path.split("#").last end diff --git a/app/domain/models/contributions/lib/measurement/idiomaticity.rb b/app/domain/models/contributions/lib/measurement/idiomaticity.rb index ffa37f5..fe61b94 100644 --- a/app/domain/models/contributions/lib/measurement/idiomaticity.rb +++ b/app/domain/models/contributions/lib/measurement/idiomaticity.rb @@ -13,19 +13,24 @@ def self.calculate(file_path) error_messages: nil } if ruby_file?(file_path) - process_source = RuboCop::ProcessedSource.from_file(file_path, ruby_version) - config = RuboCop::ConfigStore.new.for(process_source.path) - cop_classes = RuboCop::Cop::Cop.all - registry = RuboCop::Cop::Registry.new(cop_classes) - team = RuboCop::Cop::Team.new(registry, config) - result[:error_count] = team.inspect_file(process_source).count - result[:error_messages] = team.inspect_file(process_source).map(&:message) + rubocop_result = rubocop_process(file_path) + result[:error_count] = rubocop_result.count + result[:error_messages] = rubocop_result.map(&:message) end result end private + def self.rubocop_process(file_path) + process_source = RuboCop::ProcessedSource.from_file(file_path, ruby_version) + config = RuboCop::ConfigStore.new.for(process_source.path) + cop_classes = RuboCop::Cop::Cop.all + registry = RuboCop::Cop::Registry.new(cop_classes) + team = RuboCop::Cop::Team.new(registry, config) + team.inspect_file(process_source) + end + def self.ruby_version RUBY_VERSION.to_f end diff --git a/app/domain/models/contributions/lib/measurement/number_of_annotation.rb b/app/domain/models/contributions/lib/measurement/number_of_annotation.rb index a3ea12b..aa40b78 100644 --- a/app/domain/models/contributions/lib/measurement/number_of_annotation.rb +++ b/app/domain/models/contributions/lib/measurement/number_of_annotation.rb @@ -15,16 +15,20 @@ def self.calculate(lines) if is_comment?(loc) comment_index = i else - annotation += 1 if (comment_index - not_comment_index) >= LINE_LIMITATION + annotation += 1 if multiline_comment?(comment_index, not_comment_index) not_comment_index = i end end - annotation += 1 if (comment_index - not_comment_index) >= LINE_LIMITATION + annotation += 1 if multiline_comment?(comment_index, not_comment_index) annotation end private + def self.multiline_comment?(comment_index, not_comment_index) + (comment_index - not_comment_index) >= LINE_LIMITATION + end + def self.is_comment?(loc) loc.strip[0] == COMMENT end diff --git a/app/domain/models/contributions/lib/measurement/number_of_method.rb b/app/domain/models/contributions/lib/measurement/number_of_method.rb index f991c25..ebadd0a 100644 --- a/app/domain/models/contributions/lib/measurement/number_of_method.rb +++ b/app/domain/models/contributions/lib/measurement/number_of_method.rb @@ -10,40 +10,40 @@ module Measurement module NumberOfMethod - def self.calculate(contributions) - ast = Parser::CurrentRuby.parse(code(contributions)) - all_methods(ast, contributions) + def self.calculate(line_entities) + ast = Parser::CurrentRuby.parse(line_of_code(line_entities)) + all_methods_hash(ast, line_entities) end private - def self.code(contributions) - contributions.map(&:code).join("\n") + def self.line_of_code(line_entities) + line_entities.map(&:code).join("\n") end - def self.all_methods(ast, contributions) - methods = [] - find_methods(ast, methods) - return [] if methods.empty? - methods.inject([]) do |result, method_ast| + def self.all_methods_hash(ast, line_entities) + methods_ast = [] + find_methods_tree(ast, methods_ast) + return [] if methods_ast.empty? + methods_ast.inject([]) do |result, method_ast| result.push({ name: method_name(method_ast), - lines: line_contribution(method_ast, contributions) + lines: select_entity(method_ast, line_entities) }) end end - def self.line_contribution(method_ast, contributions) - method_code_array = Unparser.unparse(method_ast).split("\n") - first_number = contributions.select {|loc| loc.code.strip == method_code_array[0].strip}[0].number - end_number = first_number + method_code_array.count - 1 + def self.select_entity(method_ast, line_entities) + method_loc = Unparser.unparse(method_ast).split("\n") + first_number = line_entities.select {|loc| loc.code.strip == method_loc[0].strip}[0].number + end_number = first_number + method_loc.count - 1 result = [] while first_number <= end_number - loc = contributions.select {|loc| loc.number == first_number}[0] + loc = line_entities.select {|loc| loc.number == first_number}[0] + break if loc.nil? first_number +=1 result.push(loc) - break if loc.nil? end_number += 1 if blank_line?(loc) || comment?(loc) end result @@ -61,13 +61,13 @@ def self.method_name(method_ast) method_ast.children[0] end - def self.find_methods(ast, methods) + def self.find_methods_tree(ast, methods_ast) return nil unless Parser::AST::Node === ast if ast.type == :def - methods.append(ast) + methods_ast.append(ast) else ast.children.each do |ast| - find_methods(ast, methods) + find_methods_tree(ast, methods_ast) end end end diff --git a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb similarity index 62% rename from spec/tests_integration/measurement_integration/quality_measurement_spec.rb rename to spec/tests_integration/measurement_integration/file_level_metrics_spec.rb index 055eea6..2118c2e 100644 --- a/spec/tests_integration/measurement_integration/quality_measurement_spec.rb +++ b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb @@ -6,9 +6,9 @@ project = create(:project) git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) contributions = CodePraise::Mapper::Contributions.new(git_repo) - folder = contributions.for_folder('app') - @file = folder.files[0] - binding.pry + @folder = contributions.for_folder('app') + @file = @folder.files[0] + # binding.pry end describe "Entity::Complexity" do @@ -30,4 +30,20 @@ _(@file.total_annotations).wont_be_nil end end + + describe "Entity::MethodContribution" do + it "should return number of method" do + methods = @file.methods + _(methods.count).wont_be_nil + _(methods[0].lines).wont_be_nil + end + end + + describe "Entity::CollectiveOwnership" do + it "should return personal code ownership" do + co_array = @folder.collective_ownership + _(co_array[0].coefficient_variation).wont_be_nil + end + end + end \ No newline at end of file diff --git a/spec/tests_integration/measurement_integration/productivity_measurement.rb b/spec/tests_integration/measurement_integration/productivity_measurement.rb deleted file mode 100644 index 2491db2..0000000 --- a/spec/tests_integration/measurement_integration/productivity_measurement.rb +++ /dev/null @@ -1,20 +0,0 @@ -require_relative '../../helpers/spec_helper.rb' - -describe "Test Productivtiy Measurement" do - - before do - project = create(:project) - git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) - folder = CodePraise::Mapper::Contributions.new(git_repo).for_folder('app') - @file = folder.files[0] - binding.pry - end - - describe "Entity::MethodContribution" do - it "should return number of method" do - methods = @file.methods - _(methods.count).wont_be_nil - _(methods[0].lines).wont_be_nil - end - end -end \ No newline at end of file From 729423f29a322a05b51d3a4a49624c79daa1ccdb Mon Sep 17 00:00:00 2001 From: vicxu Date: Sat, 30 Mar 2019 17:33:25 +0800 Subject: [PATCH 08/14] calculate code ownership in file level --- .../entities/metrics/collective_ownership.rb | 17 -------- .../entities/root/folder_contributions.rb | 12 ++---- .../lib/measurement/collective_ownership.rb | 42 +++++++------------ .../contributions/values/credit_share.rb | 7 +++- .../representers/credit_share_representer.rb | 1 + .../folder_contributions_representer.rb | 2 +- .../file_level_metrics_spec.rb | 3 +- 7 files changed, 27 insertions(+), 57 deletions(-) delete mode 100644 app/domain/models/contributions/entities/metrics/collective_ownership.rb diff --git a/app/domain/models/contributions/entities/metrics/collective_ownership.rb b/app/domain/models/contributions/entities/metrics/collective_ownership.rb deleted file mode 100644 index d17f8cd..0000000 --- a/app/domain/models/contributions/entities/metrics/collective_ownership.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - module Entity - - class CollectiveOwnership - - attr_reader :contributor, :coefficient_variation, :contributions - - def initialize(contributor:, coefficient_variation:, contributions:) - @contributor = contributor - @coefficient_variation = coefficient_variation - @contributions = contributions - end - end - end -end \ No newline at end of file diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index 4b32552..44df91f 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -20,16 +20,12 @@ def initialize(path:, files:, repo_path:) end def collective_ownership - return nil unless any_subfolders? || credit_share == 0 - Measurement::CollectiveOwnership.calculate(subfolders, contributors).map do |co| - Entity::CollectiveOwnership.new( - contributor: co[:contributor], - contributions: co[:contributions], - coefficient_variation: co[:coefficient_variation] - ) - end + return nil unless any_subfolders? + Measurement::CollectiveOwnership.calculate(self) end + + def line_count files.map(&:line_count).reduce(&:+) end diff --git a/app/domain/models/contributions/lib/measurement/collective_ownership.rb b/app/domain/models/contributions/lib/measurement/collective_ownership.rb index eb610ea..2be3d0a 100644 --- a/app/domain/models/contributions/lib/measurement/collective_ownership.rb +++ b/app/domain/models/contributions/lib/measurement/collective_ownership.rb @@ -6,39 +6,27 @@ module Measurement module CollectiveOwnership - def self.calculate(sub_folders, contributor_entities) - subfolders_percentage_hash = subfolders_percentage(sub_folders, contributor_entities) - subfolders_percentage_hash.keys.inject([]) do |result, name| - result.push( - { - contributor: name, - contributions: subfolders_percentage_hash[name], - coefficient_variation: Math.coefficient_variation(percentage_nums(subfolders_percentage_hash[name])) - } - ) + def self.calculate(folder) + contributors = folder.contributors + contributors_percentage_hash = contributros_subfolders_percentage(folder.subfolders, contributors) + contributors_percentage_hash.each do |k, v| + contributors_percentage_hash[k] = Math.coefficient_variation(v) end + contributors_percentage_hash end private - - def self.subfolders_percentage(sub_folders, contributor_entities) - contributors = contributor_empty_hash(contributor_entities) - sub_folders.each do |subfolder| - contributors.each do |name, v| - share_percentage = subfolder.credit_share.share_percentage - break if share_percentage.nil? - contributors[name].push({percentage: (share_percentage[name].nil? ? 0 : share_percentage[name]), folder: subfolder.path}) + def self.contributros_subfolders_percentage(subfolders, contributors) + percentage_array = subfolders.map do |subfolder| + subfolder.credit_share.share_percentage + end + contributors_hash = contributors.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} + percentage_array.each do |subfolder_percentage| + contributors_hash.keys.each do |k| + contributors_hash[k].push(subfolder_percentage[k].nil? ? 0 : subfolder_percentage[k]) end end - contributors - end - - def self.contributor_empty_hash(contributor_entities) - contributor_entities.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} - end - - def self.percentage_nums(percentage_array) - percentage_array.map{|percentage_hash| percentage_hash[:percentage]} + contributors_hash end end end diff --git a/app/domain/models/contributions/values/credit_share.rb b/app/domain/models/contributions/values/credit_share.rb index ea79cb7..f648fea 100644 --- a/app/domain/models/contributions/values/credit_share.rb +++ b/app/domain/models/contributions/values/credit_share.rb @@ -32,9 +32,12 @@ def ==(other) def share_percentage sum = share.values.reduce(&:+) - return nil if sum.nil? || sum == 0 share.keys.inject({}) do |result, key| - result[key] = ((share[key].to_f / sum) * 100).round + if sum == 0 + result[key] = 0 + else + result[key] = ((share[key].to_f / sum) * 100).round + end result end end diff --git a/app/presentation/representers/credit_share_representer.rb b/app/presentation/representers/credit_share_representer.rb index 36afb77..31e57da 100644 --- a/app/presentation/representers/credit_share_representer.rb +++ b/app/presentation/representers/credit_share_representer.rb @@ -12,6 +12,7 @@ class CreditShare < Roar::Decorator include Roar::JSON property :share + property :share_percentage collection :contributors, extend: Representer::Contributor, class: OpenStruct end diff --git a/app/presentation/representers/folder_contributions_representer.rb b/app/presentation/representers/folder_contributions_representer.rb index 1c5dd57..bad69f3 100644 --- a/app/presentation/representers/folder_contributions_representer.rb +++ b/app/presentation/representers/folder_contributions_representer.rb @@ -20,7 +20,7 @@ class FolderContributions < Roar::Decorator property :total_credits property :any_subfolders? property :any_base_files? - collection :collective_ownership, extend: Representer::CollectiveOwnership, class: OpenStruct + property :collective_ownership property :credit_share, extend: Representer::CreditShare, class: OpenStruct collection :base_files, extend: Representer::FileContributions, class: OpenStruct collection :subfolders, extend: Representer::FolderContributions, class: OpenStruct diff --git a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb index 2118c2e..f0bb4b0 100644 --- a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb +++ b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb @@ -41,8 +41,7 @@ describe "Entity::CollectiveOwnership" do it "should return personal code ownership" do - co_array = @folder.collective_ownership - _(co_array[0].coefficient_variation).wont_be_nil + _(@folder.collective_ownership.keys).wont_be_empty end end From f7d742ce929440e598b573b3d67b21c15aca1433 Mon Sep 17 00:00:00 2001 From: vicxu Date: Sat, 30 Mar 2019 21:13:43 +0800 Subject: [PATCH 09/14] finish commit-level measurement --- app/application/services/appraise_project.rb | 7 ++- .../values/project_folder_contributions.rb | 2 +- .../entities/children/file_change.rb | 21 +++++++ .../contributions/entities/root/commit.rb | 35 +++++++++++ .../contributions/mappers/commit_diff.rb | 28 +++++++++ .../contributions/mappers/commit_mapper.rb | 59 +++++++++++++++++++ .../mappers/contributions_mapper.rb | 10 ++++ app/infrastructure/git/commit_reporter.rb | 39 ++++++++++++ .../representers/commit_representer.rb | 22 +++++++ .../representers/file_change_representer.rb | 14 +++++ ...roject_folder_contributions_representer.rb | 2 + .../commit_level_metrics_spec.rb | 32 ++++++++++ .../file_level_metrics_spec.rb | 4 +- 13 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 app/domain/models/contributions/entities/children/file_change.rb create mode 100644 app/domain/models/contributions/entities/root/commit.rb create mode 100644 app/domain/models/contributions/mappers/commit_diff.rb create mode 100644 app/domain/models/contributions/mappers/commit_mapper.rb create mode 100644 app/infrastructure/git/commit_reporter.rb create mode 100644 app/presentation/representers/commit_representer.rb create mode 100644 app/presentation/representers/file_change_representer.rb create mode 100644 spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb diff --git a/app/application/services/appraise_project.rb b/app/application/services/appraise_project.rb index e9d2d0c..4160c05 100644 --- a/app/application/services/appraise_project.rb +++ b/app/application/services/appraise_project.rb @@ -59,10 +59,11 @@ def request_cloning_worker(input) end def appraise_contributions(input) - input[:folder] = Mapper::Contributions - .new(input[:gitrepo]).for_folder(input[:requested].folder_name) + contributions = Mapper::Contributions.new(input[:gitrepo]) + input[:folder] = contributions.for_folder(input[:requested].folder_name) + input[:commits] = contributions.commits - Value::ProjectFolderContributions.new(input[:project], input[:folder]) + Value::ProjectFolderContributions.new(input[:project], input[:folder], input[:commits]) .yield_self do |appraisal| Success(Value::Result.new(status: :ok, message: appraisal)) end diff --git a/app/application/values/project_folder_contributions.rb b/app/application/values/project_folder_contributions.rb index 1da288b..3c82aea 100644 --- a/app/application/values/project_folder_contributions.rb +++ b/app/application/values/project_folder_contributions.rb @@ -3,6 +3,6 @@ module CodePraise module Value # Contributions for a folder of a project - ProjectFolderContributions = Struct.new(:project, :folder) + ProjectFolderContributions = Struct.new(:project, :folder, :commits) end end diff --git a/app/domain/models/contributions/entities/children/file_change.rb b/app/domain/models/contributions/entities/children/file_change.rb new file mode 100644 index 0000000..fd9c381 --- /dev/null +++ b/app/domain/models/contributions/entities/children/file_change.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'dry-types' +require 'dry-struct' + + +module CodePraise + module Entity + # Entity for a single line of code contributed by a team-member + class FileChange < Dry::Struct + + include Dry::Types.module + + attribute :path, Strict::String + attribute :name, Strict::String + attribute :addition, Strict::Integer + attribute :deletion, Strict::Integer + + end + end +end diff --git a/app/domain/models/contributions/entities/root/commit.rb b/app/domain/models/contributions/entities/root/commit.rb new file mode 100644 index 0000000..f9df887 --- /dev/null +++ b/app/domain/models/contributions/entities/root/commit.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'dry-types' +require 'dry-struct' +require_relative '../children/contributor' + + +module CodePraise + module Entity + # Entity for a single line of code contributed by a team-member + class Commit < Dry::Struct + + include Dry::Types.module + + attribute :committer, Contributor + attribute :sha, Strict::String + attribute :date, Params::DateTime + attribute :size, Strict::Integer + attribute :message, Strict::String + attribute :file_changes, Strict::Array.of(FileChange) + + def total_additions + file_changes.map(&:addition).reduce(&:+) + end + + def total_deletions + file_changes.map(&:deletion).reduce(&:+) + end + + def total_files + file_changes.count + end + end + end +end diff --git a/app/domain/models/contributions/mappers/commit_diff.rb b/app/domain/models/contributions/mappers/commit_diff.rb new file mode 100644 index 0000000..c61ab5e --- /dev/null +++ b/app/domain/models/contributions/mappers/commit_diff.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module CodePraise + module Mapper + # Parses git blame porcelain: https://git-scm.com/docs/git-blame/1.6.0 + module CommitDiff + + def self.parser(diff) + diff_files = diff.stats[:files] + diff_files.keys.map do |key| + { + path: key, + name: file_name(key), + addition: diff_files[key][:insertions], + deletion: diff_files[key][:deletions] + } + end + end + + private + + def self.file_name(file_path) + file_path.split("/").last + end + + end + end +end diff --git a/app/domain/models/contributions/mappers/commit_mapper.rb b/app/domain/models/contributions/mappers/commit_mapper.rb new file mode 100644 index 0000000..4a27cfa --- /dev/null +++ b/app/domain/models/contributions/mappers/commit_mapper.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative 'commit_diff' + +module CodePraise + module Mapper + class Commit + + def initialize(commit, empty_commit) + @commit = commit + @empty_commit = empty_commit + end + + def build_entity + Entity::Commit.new( + committer: committer, + sha: @commit.sha, + date: @commit.date, + message: @commit.message, + size: @commit.size, + file_changes: file_changes + ) + end + + + private + + def committer + Entity::Contributor.new( + username: @commit.committer.name, + email: @commit.committer.email + ) + end + + def file_changes + file_change_array = CommitDiff.parser(get_diff) + file_change_array.each do |file_change| + Entity::FileChange.new( + path: file_change[:path], + name: file_change[:name], + addition: file_change[:addition], + deletion: file_change[:deletion], + modification: file_change[:modification] + ) + end + end + + def get_diff + if @commit.parent.nil? + @empty_commit.diff(@commit) + else + @commit.parent.diff(@commit) + end + end + + + end + end +end diff --git a/app/domain/models/contributions/mappers/contributions_mapper.rb b/app/domain/models/contributions/mappers/contributions_mapper.rb index 8ceb3de..9b07bc6 100644 --- a/app/domain/models/contributions/mappers/contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/contributions_mapper.rb @@ -18,6 +18,16 @@ def for_folder(folder_name) ).build_entity end + def commits(since=nil) + commit_report = GitCommit::CommitReporter.new(@gitrepo) + commits = commit_report.commits(since) + empty_commit = commit_report.empty_commit + + commits.map do |commit| + Mapper::Commit.new(commit, empty_commit).build_entity + end + end + def parse_file_reports(blame_output) blame_output.map do |file_blame| name = file_blame[0] diff --git a/app/infrastructure/git/commit_reporter.rb b/app/infrastructure/git/commit_reporter.rb new file mode 100644 index 0000000..8239fd6 --- /dev/null +++ b/app/infrastructure/git/commit_reporter.rb @@ -0,0 +1,39 @@ +require 'git' + + +module GitCommit + class CommitReporter + + EMPTY_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' + + def initialize(gitrepo) + @local = gitrepo.local + @git = Git.open(@local.git_repo_path) + end + + def commit(sha) + @git.log(sha) + end + + def commits(since=nil) + commits = get_all_commits + commits = commits.since(since) unless since.nil? + commits + end + + def empty_commit + @git.gcommit(EMPTY_SHA) + end + + private + + def get_all_commits + n = 500 + until @git.log(n).size < n do + n += 500 + end + @git.log(n) + end + + end +end \ No newline at end of file diff --git a/app/presentation/representers/commit_representer.rb b/app/presentation/representers/commit_representer.rb new file mode 100644 index 0000000..ca48812 --- /dev/null +++ b/app/presentation/representers/commit_representer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative 'contributor_representer' +require_relative 'file_change_representer' + +module CodePraise + module Representer + class Commit < Roar::Decorator + include Roar::JSON + + property :committer, extend: Representer::Contributor, class: OpenStruct + property :sha + property :message + property :date + property :size + property :total_additions + property :total_deletions + property :total_files + collection :file_changes, extend: Representer::FileChange, class: OpenStruct + end + end +end diff --git a/app/presentation/representers/file_change_representer.rb b/app/presentation/representers/file_change_representer.rb new file mode 100644 index 0000000..e6e5546 --- /dev/null +++ b/app/presentation/representers/file_change_representer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module CodePraise + module Representer + class FileChange < Roar::Decorator + include Roar::JSON + + property :path + property :name + property :addition + property :deletion + end + end +end diff --git a/app/presentation/representers/project_folder_contributions_representer.rb b/app/presentation/representers/project_folder_contributions_representer.rb index df5feaa..0c4731e 100644 --- a/app/presentation/representers/project_folder_contributions_representer.rb +++ b/app/presentation/representers/project_folder_contributions_representer.rb @@ -5,6 +5,7 @@ require_relative 'folder_contributions_representer' require_relative 'project_representer' +require_relative 'commit_representer' module CodePraise module Representer @@ -14,6 +15,7 @@ class ProjectFolderContributions < Roar::Decorator property :project, extend: Representer::Project, class: OpenStruct property :folder, extend: Representer::FolderContributions, class: OpenStruct + collection :commits, extend: Representer::Commit, class: OpenStruct end end end diff --git a/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb new file mode 100644 index 0000000..22729c4 --- /dev/null +++ b/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb @@ -0,0 +1,32 @@ +require_relative '../../helpers/spec_helper.rb' + +describe "Test Commit-Level Measurement" do + + before do + project = create(:project) + git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) + contributions = CodePraise::Mapper::Contributions.new(git_repo) + @commits = contributions.commits + end + + describe "Entity::Commit" do + it "should return commit information" do + commit = @commits[0] + _(@commits.size).must_be :>, 0 + _(commit.total_additions).must_be :>, 0 + _(commit.total_deletions).must_be :>, 0 + _(commit.total_files).must_be :>, 0 + end + end + + describe "Entity::FileChange" do + it "should return addition, deletion and file information" do + file_change = @commits[0].file_changes[0] + _(file_change.addition).wont_be_nil + _(file_change.deletion).wont_be_nil + _(file_change.path).wont_be_nil + _(file_change.name).wont_be_nil + end + end + +end \ No newline at end of file diff --git a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb index f0bb4b0..237cfb2 100644 --- a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb +++ b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb @@ -1,6 +1,6 @@ require_relative '../../helpers/spec_helper.rb' -describe "Test Quality Measurement" do +describe "Test File-Level Measurement" do before do project = create(:project) @@ -8,7 +8,7 @@ contributions = CodePraise::Mapper::Contributions.new(git_repo) @folder = contributions.for_folder('app') @file = @folder.files[0] - # binding.pry + binding.pry end describe "Entity::Complexity" do From 778e0a6c9bd54f75d8ced71b012add3da35000dc Mon Sep 17 00:00:00 2001 From: vicxu Date: Mon, 1 Apr 2019 14:59:09 +0800 Subject: [PATCH 10/14] move measurement module to another module --- .../entities/children/complexity.rb | 15 ++++++ .../entities/children/file_contributions.rb | 6 +-- .../entities/metrics/complexity.rb | 17 ------- .../entities/metrics/idiomaticity.rb | 4 -- .../entities/root/folder_contributions.rb | 10 +--- .../lib/code_ownership_calculator.rb | 34 ++++++++++++++ .../contributions/lib/comment_calculator.rb | 45 ++++++++++++++++++ .../lib/measurement/collective_ownership.rb | 21 --------- .../lib/measurement/complexity.rb | 46 ------------------- .../lib/measurement/number_of_annotation.rb | 38 --------------- .../contributions/mappers/commit_diff.rb | 7 +-- .../mappers/complexity_mapper.rb | 30 ++++++++++++ .../mappers/file_contributions_mapper.rb | 20 ++------ .../mappers/method_contribution_mapper.rb | 6 +-- .../method_parser.rb} | 41 ++++++++--------- app/infrastructure/complexity/abc_metric.rb | 46 +++++++++++++++++++ app/infrastructure/complexity/complexity.rb | 16 +++++++ app/infrastructure/complexity/init.rb | 5 ++ app/infrastructure/init.rb | 2 +- .../collective_ownership_representer.rb | 16 ------- .../representers/complexity_representer.rb | 2 +- .../file_contributions_representer.rb | 3 +- .../folder_contributions_representer.rb | 3 +- 23 files changed, 228 insertions(+), 205 deletions(-) create mode 100644 app/domain/models/contributions/entities/children/complexity.rb delete mode 100644 app/domain/models/contributions/entities/metrics/complexity.rb create mode 100644 app/domain/models/contributions/lib/code_ownership_calculator.rb create mode 100644 app/domain/models/contributions/lib/comment_calculator.rb delete mode 100644 app/domain/models/contributions/lib/measurement/complexity.rb delete mode 100644 app/domain/models/contributions/lib/measurement/number_of_annotation.rb create mode 100644 app/domain/models/contributions/mappers/complexity_mapper.rb rename app/domain/models/contributions/{lib/measurement/number_of_method.rb => mappers/method_parser.rb} (68%) create mode 100644 app/infrastructure/complexity/abc_metric.rb create mode 100644 app/infrastructure/complexity/complexity.rb create mode 100644 app/infrastructure/complexity/init.rb delete mode 100644 app/presentation/representers/collective_ownership_representer.rb diff --git a/app/domain/models/contributions/entities/children/complexity.rb b/app/domain/models/contributions/entities/children/complexity.rb new file mode 100644 index 0000000..cb21ccb --- /dev/null +++ b/app/domain/models/contributions/entities/children/complexity.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module CodePraise + module Entity + # Complexity for file and methods of this file + class Complexity + attr_reader :average, :methods + + def initialize(average:, methods:) + @average = average + @methods = methods + end + end + end +end diff --git a/app/domain/models/contributions/entities/children/file_contributions.rb b/app/domain/models/contributions/entities/children/file_contributions.rb index 3020613..fb2bd66 100644 --- a/app/domain/models/contributions/entities/children/file_contributions.rb +++ b/app/domain/models/contributions/entities/children/file_contributions.rb @@ -5,21 +5,21 @@ module Entity # Entity for file contributions class FileContributions include Mixins::ContributionsCalculator + include Mixins::CommentCalculator DOT = '\.' LINE_END = '$' WANTED_EXTENSION = %w[rb js css html slim md].join('|') EXTENSION_REGEX = /#{DOT}#{WANTED_EXTENSION}#{LINE_END}/.freeze - attr_reader :file_path, :lines, :complexity, :idiomaticity, :methods, :total_annotations + attr_reader :file_path, :lines, :complexity, :idiomaticity, :methods - def initialize(file_path:, lines:, complexity:, idiomaticity:, methods:, total_annotations:) + def initialize(file_path:, lines:, complexity:, idiomaticity:, methods:) @file_path = Value::FilePath.new(file_path) @lines = lines @complexity = complexity @idiomaticity = idiomaticity @methods = methods - @total_annotations = total_annotations end def total_credits diff --git a/app/domain/models/contributions/entities/metrics/complexity.rb b/app/domain/models/contributions/entities/metrics/complexity.rb deleted file mode 100644 index 4535e24..0000000 --- a/app/domain/models/contributions/entities/metrics/complexity.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - - module Entity - - class Complexity - - attr_reader :average, :methods_complexity - - def initialize(average: ,methods_complexity: ) - @average = average - @methods_complexity = methods_complexity - end - end - end -end diff --git a/app/domain/models/contributions/entities/metrics/idiomaticity.rb b/app/domain/models/contributions/entities/metrics/idiomaticity.rb index ddc69bf..93d80bf 100644 --- a/app/domain/models/contributions/entities/metrics/idiomaticity.rb +++ b/app/domain/models/contributions/entities/metrics/idiomaticity.rb @@ -4,16 +4,12 @@ require 'dry-struct' module CodePraise - module Entity - class Idiomaticity < Dry::Struct - include Dry::Types.module attribute :error_count, Strict::Integer.optional attribute :error_messages, Strict::Array.of(Strict::String).optional - end end end diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index 44df91f..225ce2b 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true -require_relative '../../lib/measurement/collective_ownership' + module CodePraise module Entity # Entity for folder contributions class FolderContributions < SimpleDelegator include Mixins::ContributionsCalculator + include Mixins::CodeOnwershipCalculator attr_reader :path, :files, :repo_path @@ -19,13 +20,6 @@ def initialize(path:, files:, repo_path:) subfolders.each { |folder| self[folder.path] = folder } end - def collective_ownership - return nil unless any_subfolders? - Measurement::CollectiveOwnership.calculate(self) - end - - - def line_count files.map(&:line_count).reduce(&:+) end diff --git a/app/domain/models/contributions/lib/code_ownership_calculator.rb b/app/domain/models/contributions/lib/code_ownership_calculator.rb new file mode 100644 index 0000000..8a7e959 --- /dev/null +++ b/app/domain/models/contributions/lib/code_ownership_calculator.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module CodePraise + module Mixins + # Calculate coefficient variantion of subfolder contribution + module CodeOnwershipCalculator + def coefficient_variation + return nil unless any_subfolders? + + contributors_percentage_hash = contributros_subfolders_percentage + contributors_percentage_hash.each do |k, v| + contributors_percentage_hash[k] = Math.coefficient_variation(v) + end + contributors_percentage_hash + end + + private + + def contributros_subfolders_percentage + percentage_array = subfolders.map do |subfolder| + subfolder.credit_share.share_percentage + end + contributors_hash = contributors.each_with_object({}) {|contributor, hash| hash[contributor.username] = []; hash} + percentage_array.each do |subfolder_percentage| + contributors_hash.keys.each do |k| + contributors_hash[k] + .push(subfolder_percentage[k].nil? ? 0 : subfolder_percentage[k]) + end + end + contributors_hash + end + end + end +end diff --git a/app/domain/models/contributions/lib/comment_calculator.rb b/app/domain/models/contributions/lib/comment_calculator.rb new file mode 100644 index 0000000..978d4ef --- /dev/null +++ b/app/domain/models/contributions/lib/comment_calculator.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module CodePraise + module Mixins + # Comment and multiline comment calculator + module CommentCalculator + MULTILINE = 2 + COMMENT = '#' + + def multiline_comments + comment_index = not_comment_index = documentation = 0 + lines_of_code.each_with_index do |loc, i| + if comment?(loc) + comment_index = i + else + documentation += 1 if multiline_comment?(comment_index, not_comment_index) + not_comment_index = i + end + end + documentation += 1 if multiline_comment?(comment_index, not_comment_index) + documentation + end + + def comments + lines_of_code.select do |loc| + comment?(loc) + end.count + end + + private + + def lines_of_code + lines.map(&:code) + end + + def multiline_comment?(comment_index, not_comment_index) + (comment_index - not_comment_index) >= MULTILINE + end + + def comment?(loc) + loc.strip[0] == COMMENT + end + end + end +end diff --git a/app/domain/models/contributions/lib/measurement/collective_ownership.rb b/app/domain/models/contributions/lib/measurement/collective_ownership.rb index 2be3d0a..9b2cbb7 100644 --- a/app/domain/models/contributions/lib/measurement/collective_ownership.rb +++ b/app/domain/models/contributions/lib/measurement/collective_ownership.rb @@ -6,28 +6,7 @@ module Measurement module CollectiveOwnership - def self.calculate(folder) - contributors = folder.contributors - contributors_percentage_hash = contributros_subfolders_percentage(folder.subfolders, contributors) - contributors_percentage_hash.each do |k, v| - contributors_percentage_hash[k] = Math.coefficient_variation(v) - end - contributors_percentage_hash - end - private - def self.contributros_subfolders_percentage(subfolders, contributors) - percentage_array = subfolders.map do |subfolder| - subfolder.credit_share.share_percentage - end - contributors_hash = contributors.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} - percentage_array.each do |subfolder_percentage| - contributors_hash.keys.each do |k| - contributors_hash[k].push(subfolder_percentage[k].nil? ? 0 : subfolder_percentage[k]) - end - end - contributors_hash - end end end end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/complexity.rb b/app/domain/models/contributions/lib/measurement/complexity.rb deleted file mode 100644 index 169c180..0000000 --- a/app/domain/models/contributions/lib/measurement/complexity.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'flog' - -module CodePraise - - module Measurement - - module Complexity - - def self.calculate(file_path) - result = { - average: nil, - methods_complexity: nil - } - if ruby_file?(file_path) - flog_result = flog_process(file_path) - result[:average] = flog_result.average - result[:methods_complexity] = methods_complexity(flog_result.totals) - end - result - end - - private - - def self.flog_process(file_path) - flog = Flog.new - flog.flog(*file_path) - flog - end - - def self.methods_complexity(flog_totals) - flog_totals.keys.inject({}) do |result, key| - result[only_method_name(key)] = flog_totals[key] - result - end - end - - def self.only_method_name(file_path) - file_path.split("#").last - end - - def self.ruby_file?(file_path) - File.extname(file_path) == '.rb' - end - end - end -end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/number_of_annotation.rb b/app/domain/models/contributions/lib/measurement/number_of_annotation.rb deleted file mode 100644 index aa40b78..0000000 --- a/app/domain/models/contributions/lib/measurement/number_of_annotation.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - - module Measurement - - module NumberOfAnnotation - - LINE_LIMITATION = 2 - COMMENT = "#" - - def self.calculate(lines) - comment_index, not_comment_index, annotation = [0, 0, 0] - lines.each_with_index do |loc, i| - if is_comment?(loc) - comment_index = i - else - annotation += 1 if multiline_comment?(comment_index, not_comment_index) - not_comment_index = i - end - end - annotation += 1 if multiline_comment?(comment_index, not_comment_index) - annotation - end - - private - - def self.multiline_comment?(comment_index, not_comment_index) - (comment_index - not_comment_index) >= LINE_LIMITATION - end - - def self.is_comment?(loc) - loc.strip[0] == COMMENT - end - - end - end -end \ No newline at end of file diff --git a/app/domain/models/contributions/mappers/commit_diff.rb b/app/domain/models/contributions/mappers/commit_diff.rb index c61ab5e..9010645 100644 --- a/app/domain/models/contributions/mappers/commit_diff.rb +++ b/app/domain/models/contributions/mappers/commit_diff.rb @@ -2,9 +2,7 @@ module CodePraise module Mapper - # Parses git blame porcelain: https://git-scm.com/docs/git-blame/1.6.0 module CommitDiff - def self.parser(diff) diff_files = diff.stats[:files] diff_files.keys.map do |key| @@ -17,12 +15,9 @@ def self.parser(diff) end end - private - def self.file_name(file_path) - file_path.split("/").last + file_path.split('/').last end - end end end diff --git a/app/domain/models/contributions/mappers/complexity_mapper.rb b/app/domain/models/contributions/mappers/complexity_mapper.rb new file mode 100644 index 0000000..70ec840 --- /dev/null +++ b/app/domain/models/contributions/mappers/complexity_mapper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module CodePraise + module Mapper + # Transform Flog raw data into Complexity Entity + class Complexity + def initialize(file_path) + @file_path = file_path + end + + def build_entity + complexity = abc_metric + + return nil if complexity.nil? + + Entity::Complexity.new( + average: complexity[:average], + methods: complexity[:methods] + ) + end + + private + + def abc_metric + abc_metric = CodePraise::Complexity::AbcMetric.new(@file_path) + CodePraise::Complexity::Calculator.new(abc_metric).calculate + end + end + end +end \ No newline at end of file diff --git a/app/domain/models/contributions/mappers/file_contributions_mapper.rb b/app/domain/models/contributions/mappers/file_contributions_mapper.rb index 602ba9b..757a730 100644 --- a/app/domain/models/contributions/mappers/file_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/file_contributions_mapper.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -require_relative '../lib/measurement/complexity' +require_relative 'complexity_mapper' require_relative '../lib/measurement/idiomaticity' -require_relative '../lib/measurement/number_of_annotation' module CodePraise module Mapper @@ -19,8 +18,7 @@ def build_entity lines: contributions, complexity: complexity, idiomaticity: idiomaticity, - methods: methods, - total_annotations: total_annotations + methods: methods ) end @@ -35,12 +33,7 @@ def contributions end def complexity - file_path = Value::FilePath.new(filename) - complexity_hash = Measurement::Complexity.calculate("#{@repo_path}/#{file_path}") - Entity::Complexity.new( - average: complexity_hash[:average], - methods_complexity: complexity_hash[:methods_complexity] - ) + Mapper::Complexity.new("#{@repo_path}/#{filename}").build_entity end def idiomaticity @@ -52,10 +45,6 @@ def idiomaticity ) end - def total_annotations - Measurement::NumberOfAnnotation.calculate(contributions.map(&:code)) - end - def summarize_line_reports(line_reports) line_reports.map.with_index do |report, line_index| Entity::LineContribution.new( @@ -69,11 +58,12 @@ def summarize_line_reports(line_reports) def methods return [] unless ruby_file? + MethodContributions.new(contributions).build_entity end def ruby_file? - File.extname(@file_report[0]) == ".rb" + File.extname(@file_report[0]) == '.rb' end def contributor_from(report) diff --git a/app/domain/models/contributions/mappers/method_contribution_mapper.rb b/app/domain/models/contributions/mappers/method_contribution_mapper.rb index cb28358..41b4e1a 100644 --- a/app/domain/models/contributions/mappers/method_contribution_mapper.rb +++ b/app/domain/models/contributions/mappers/method_contribution_mapper.rb @@ -1,9 +1,7 @@ -require_relative '../lib/measurement/number_of_method' +# frozen_string_literal: true module CodePraise - module Mapper - class MethodContributions def initialize(file_contributions) @file_contributions = file_contributions @@ -21,7 +19,7 @@ def build_entity private def all_methods - Measurement::NumberOfMethod.calculate(@file_contributions) + MethodParser.parse_methods(@file_contributions) end end end diff --git a/app/domain/models/contributions/lib/measurement/number_of_method.rb b/app/domain/models/contributions/mappers/method_parser.rb similarity index 68% rename from app/domain/models/contributions/lib/measurement/number_of_method.rb rename to app/domain/models/contributions/mappers/method_parser.rb index ebadd0a..be80945 100644 --- a/app/domain/models/contributions/lib/measurement/number_of_method.rb +++ b/app/domain/models/contributions/mappers/method_parser.rb @@ -3,34 +3,25 @@ require 'parser/current' require 'unparser' - module CodePraise - - module Measurement - - module NumberOfMethod - - def self.calculate(line_entities) + module Mapper + # Find all method in a file + module MethodParser + def self.parse_methods(line_entities) ast = Parser::CurrentRuby.parse(line_of_code(line_entities)) all_methods_hash(ast, line_entities) end - private - def self.line_of_code(line_entities) line_entities.map(&:code).join("\n") end - def self.all_methods_hash(ast, line_entities) methods_ast = [] find_methods_tree(ast, methods_ast) - return [] if methods_ast.empty? methods_ast.inject([]) do |result, method_ast| - result.push({ - name: method_name(method_ast), - lines: select_entity(method_ast, line_entities) - }) + result.push(name: method_name(method_ast), + lines: select_entity(method_ast, line_entities)) end end @@ -40,15 +31,18 @@ def self.select_entity(method_ast, line_entities) end_number = first_number + method_loc.count - 1 result = [] while first_number <= end_number - loc = line_entities.select {|loc| loc.number == first_number}[0] - break if loc.nil? - first_number +=1 + loc = line_entities.select do |line_entity| + line_entity.number == first_number + end.first + first_number += 1 result.push(loc) end_number += 1 if blank_line?(loc) || comment?(loc) end result end + private + def self.blank_line?(loc) loc.code.strip.empty? end @@ -62,16 +56,19 @@ def self.method_name(method_ast) end def self.find_methods_tree(ast, methods_ast) - return nil unless Parser::AST::Node === ast + return nil unless ast.is_a?(Parser::AST::Node) + if ast.type == :def methods_ast.append(ast) else - ast.children.each do |ast| - find_methods_tree(ast, methods_ast) + ast.children.each do |child_ast| + find_methods_tree(child_ast, methods_ast) end end end + private_class_method :find_methods_tree, :comment?, :blank_line?, + :method_name end end -end \ No newline at end of file +end diff --git a/app/infrastructure/complexity/abc_metric.rb b/app/infrastructure/complexity/abc_metric.rb new file mode 100644 index 0000000..28e7797 --- /dev/null +++ b/app/infrastructure/complexity/abc_metric.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'flog' + +module CodePraise + module Complexity + # ABC Metric calculation class + class AbcMetric + def initialize(file_path) + @file_path = file_path + end + + def calculate + return nil unless ruby_file? + + flog_result = flog_process + { + average: flog_result.average, + methods: methods_complexity(flog_result.totals) + } + end + + private + + def flog_process + flog = Flog.new + flog.flog(*@file_path) + flog + end + + def ruby_file? + File.extname(@file_path) == '.rb' + end + + def methods_complexity(flog_totals) + flog_totals.keys.each_with_object({}) do |key, result| + result[only_method_name(key)] = flog_totals[key] + end + end + + def only_method_name(file_path) + file_path.split('#').last + end + end + end +end diff --git a/app/infrastructure/complexity/complexity.rb b/app/infrastructure/complexity/complexity.rb new file mode 100644 index 0000000..05cc557 --- /dev/null +++ b/app/infrastructure/complexity/complexity.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module CodePraise + module Complexity + # Complexity Calculator + class Calculator + def initialize(adapter) + @adapter = adapter + end + + def calculate + @adapter.calculate + end + end + end +end diff --git a/app/infrastructure/complexity/init.rb b/app/infrastructure/complexity/init.rb new file mode 100644 index 0000000..50d81c0 --- /dev/null +++ b/app/infrastructure/complexity/init.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Dir.glob("#{File.dirname(__FILE__)}/*.rb").each do |file| + require file +end diff --git a/app/infrastructure/init.rb b/app/infrastructure/init.rb index 6ea203c..0e004fe 100644 --- a/app/infrastructure/init.rb +++ b/app/infrastructure/init.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -folders = %w[github database git cache messaging] +folders = %w[github database git cache messaging complexity] folders.each do |folder| require_relative "#{folder}/init.rb" end diff --git a/app/presentation/representers/collective_ownership_representer.rb b/app/presentation/representers/collective_ownership_representer.rb deleted file mode 100644 index 0f56e6b..0000000 --- a/app/presentation/representers/collective_ownership_representer.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'roar/decorator' -require 'roar/json' - - -module CodePraise - module Representer - # Represents folder summary about repo's folder - class CollectiveOwnership < Roar::Decorator - include Roar::JSON - - property :contributor - collection :contributions - property :coefficient_variation - end - end -end diff --git a/app/presentation/representers/complexity_representer.rb b/app/presentation/representers/complexity_representer.rb index bec97c3..be53eab 100644 --- a/app/presentation/representers/complexity_representer.rb +++ b/app/presentation/representers/complexity_representer.rb @@ -8,7 +8,7 @@ class Complexity < Roar::Decorator include Roar::JSON property :average - property :methods_complexity + property :methods end end end diff --git a/app/presentation/representers/file_contributions_representer.rb b/app/presentation/representers/file_contributions_representer.rb index 888acb7..0d67fc1 100644 --- a/app/presentation/representers/file_contributions_representer.rb +++ b/app/presentation/representers/file_contributions_representer.rb @@ -20,7 +20,8 @@ class FileContributions < Roar::Decorator property :line_count property :total_credits property :total_methods - property :total_annotations + property :multiline_comments + property :comments collection :methods, extend: Representer::MethodContributions, class: OpenStruct property :file_path, extend: Representer::FilePath, class: OpenStruct property :credit_share, extend: Representer::CreditShare, class: OpenStruct diff --git a/app/presentation/representers/folder_contributions_representer.rb b/app/presentation/representers/folder_contributions_representer.rb index bad69f3..a9a9f9e 100644 --- a/app/presentation/representers/folder_contributions_representer.rb +++ b/app/presentation/representers/folder_contributions_representer.rb @@ -7,7 +7,6 @@ require_relative 'credit_share_representer' require_relative 'file_contributions_representer' require_relative 'line_contribution_representer' -require_relative 'collective_ownership_representer' module CodePraise module Representer @@ -20,7 +19,7 @@ class FolderContributions < Roar::Decorator property :total_credits property :any_subfolders? property :any_base_files? - property :collective_ownership + property :coefficient_variation property :credit_share, extend: Representer::CreditShare, class: OpenStruct collection :base_files, extend: Representer::FileContributions, class: OpenStruct collection :subfolders, extend: Representer::FolderContributions, class: OpenStruct From 39ac186d25d7c6b1609ff4808371a2cd6aeb46a8 Mon Sep 17 00:00:00 2001 From: vicxu Date: Mon, 8 Apr 2019 10:58:39 +0800 Subject: [PATCH 11/14] create flog and rubocop in infrastructure --- .rubocop.yml | 2 +- app/application/services/appraise_project.rb | 1 - .../{metrics => children}/idiomaticity.rb | 4 +- .../entities/children/offense.rb | 16 +++++ .../entities/root/folder_contributions.rb | 11 ---- .../lib/code_ownership_calculator.rb | 6 +- .../lib/contributions_calculator.rb | 2 +- .../lib/measurement/collective_ownership.rb | 12 ---- .../lib/measurement/idiomaticity.rb | 43 -------------- .../mappers/complexity_mapper.rb | 4 +- .../mappers/file_contributions_mapper.rb | 12 +--- .../mappers/folder_contributions_mapper.rb | 4 +- .../mappers/idiomaticity_mapper.rb | 31 ++++++++++ .../mappers/method_contribution_mapper.rb | 6 +- .../contributions/mappers/method_parser.rb | 59 ++++++++++++------- .../models/contributions/values/file_path.rb | 2 +- app/infrastructure/complexity/complexity.rb | 16 ----- .../abc_metric.rb => flog/flog.rb} | 4 +- .../{complexity => flog}/init.rb | 0 app/infrastructure/init.rb | 2 +- app/infrastructure/rubocop/command.rb | 51 ++++++++++++++++ app/infrastructure/rubocop/init.rb | 5 ++ app/infrastructure/rubocop/reporter.rb | 36 +++++++++++ .../representers/idiomaticity_representer.rb | 9 ++- .../representers/offense_representer.rb | 17 ++++++ lib/math_extension.rb | 7 +++ spec/factories/project_factory.rb | 12 ++-- .../commit_level_metrics_spec.rb | 13 ++-- .../file_level_metrics_spec.rb | 48 ++++----------- spec/tests_unit/rubocop_spec.rb | 31 ++++++++++ workers/shoryuken_dev.yml | 2 +- workers/shoryuken_test.yml | 2 +- 32 files changed, 287 insertions(+), 183 deletions(-) rename app/domain/models/contributions/entities/{metrics => children}/idiomaticity.rb (59%) create mode 100644 app/domain/models/contributions/entities/children/offense.rb delete mode 100644 app/domain/models/contributions/lib/measurement/collective_ownership.rb delete mode 100644 app/domain/models/contributions/lib/measurement/idiomaticity.rb create mode 100644 app/domain/models/contributions/mappers/idiomaticity_mapper.rb delete mode 100644 app/infrastructure/complexity/complexity.rb rename app/infrastructure/{complexity/abc_metric.rb => flog/flog.rb} (95%) rename app/infrastructure/{complexity => flog}/init.rb (100%) create mode 100644 app/infrastructure/rubocop/command.rb create mode 100644 app/infrastructure/rubocop/init.rb create mode 100644 app/infrastructure/rubocop/reporter.rb create mode 100644 app/presentation/representers/offense_representer.rb create mode 100644 spec/tests_unit/rubocop_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 38f79a8..6966b53 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ AllCops: Exclude: - '_*/**/*' # - '_snippets/**/*' - - 'app/infrastructure/git/repostore/**/*' + # - 'app/infrastructure/git/repostore/**/*' # ignore block length in non-production code Metrics/BlockLength: diff --git a/app/application/services/appraise_project.rb b/app/application/services/appraise_project.rb index 4160c05..48cb7b9 100644 --- a/app/application/services/appraise_project.rb +++ b/app/application/services/appraise_project.rb @@ -26,7 +26,6 @@ def find_project_details(input) input[:project] = Repository::For.klass(Entity::Project).find_full_name( input[:requested].owner_name, input[:requested].project_name ) - if input[:project] Success(input) else diff --git a/app/domain/models/contributions/entities/metrics/idiomaticity.rb b/app/domain/models/contributions/entities/children/idiomaticity.rb similarity index 59% rename from app/domain/models/contributions/entities/metrics/idiomaticity.rb rename to app/domain/models/contributions/entities/children/idiomaticity.rb index 93d80bf..42f8c28 100644 --- a/app/domain/models/contributions/entities/metrics/idiomaticity.rb +++ b/app/domain/models/contributions/entities/children/idiomaticity.rb @@ -2,14 +2,14 @@ require 'dry-types' require 'dry-struct' +require_relative 'offense' module CodePraise module Entity class Idiomaticity < Dry::Struct include Dry::Types.module - attribute :error_count, Strict::Integer.optional - attribute :error_messages, Strict::Array.of(Strict::String).optional + attribute :offenses, Strict::Array.of(Entity::Offense).optional end end end diff --git a/app/domain/models/contributions/entities/children/offense.rb b/app/domain/models/contributions/entities/children/offense.rb new file mode 100644 index 0000000..00b2797 --- /dev/null +++ b/app/domain/models/contributions/entities/children/offense.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'dry-types' +require 'dry-struct' + +module CodePraise + module Entity + class Offense < Dry::Struct + include Dry::Types.module + + attribute :type, Strict::String + attribute :message, Strict::String + attribute :location, Strict::Integer + end + end +end diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index 225ce2b..ca05d72 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -68,17 +68,6 @@ def contributors credit_share.contributors end - def subfolder_contributions - result = credit_share.contributors.inject({}) {|hash, contributor| hash[contributor.username] = []; hash} - subfolders.each do |subfolder| - result.each do |k, v| - percentage = subfolder.credit_share.percentage - result[k].push({percentage: (percentage[k].nil? ? 0.0 : percentage[k]), path: subfolder.path}) - end - end - result - end - private def comparitive_path diff --git a/app/domain/models/contributions/lib/code_ownership_calculator.rb b/app/domain/models/contributions/lib/code_ownership_calculator.rb index 8a7e959..fba62fc 100644 --- a/app/domain/models/contributions/lib/code_ownership_calculator.rb +++ b/app/domain/models/contributions/lib/code_ownership_calculator.rb @@ -9,7 +9,8 @@ def coefficient_variation contributors_percentage_hash = contributros_subfolders_percentage contributors_percentage_hash.each do |k, v| - contributors_percentage_hash[k] = Math.coefficient_variation(v) + nums = v.is_a?(Array) ? v : [v] + contributors_percentage_hash[k] = Math.coefficient_variation(nums) end contributors_percentage_hash end @@ -23,8 +24,7 @@ def contributros_subfolders_percentage contributors_hash = contributors.each_with_object({}) {|contributor, hash| hash[contributor.username] = []; hash} percentage_array.each do |subfolder_percentage| contributors_hash.keys.each do |k| - contributors_hash[k] - .push(subfolder_percentage[k].nil? ? 0 : subfolder_percentage[k]) + contributors_hash[k].push(subfolder_percentage[k].nil? ? 0 : subfolder_percentage[k]) end end contributors_hash diff --git a/app/domain/models/contributions/lib/contributions_calculator.rb b/app/domain/models/contributions/lib/contributions_calculator.rb index 95ac4ec..77b99f4 100644 --- a/app/domain/models/contributions/lib/contributions_calculator.rb +++ b/app/domain/models/contributions/lib/contributions_calculator.rb @@ -25,7 +25,7 @@ def percent_credit_of(contributor) end def total_methods - @methods.count + @methods.nil? ? 0 : @methods.count end end end diff --git a/app/domain/models/contributions/lib/measurement/collective_ownership.rb b/app/domain/models/contributions/lib/measurement/collective_ownership.rb deleted file mode 100644 index 9b2cbb7..0000000 --- a/app/domain/models/contributions/lib/measurement/collective_ownership.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - - module Measurement - - module CollectiveOwnership - - - end - end -end \ No newline at end of file diff --git a/app/domain/models/contributions/lib/measurement/idiomaticity.rb b/app/domain/models/contributions/lib/measurement/idiomaticity.rb deleted file mode 100644 index fe61b94..0000000 --- a/app/domain/models/contributions/lib/measurement/idiomaticity.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true -require 'rubocop' - -module CodePraise - - module Measurement - - module Idiomaticity - - def self.calculate(file_path) - result = { - error_count: nil, - error_messages: nil - } - if ruby_file?(file_path) - rubocop_result = rubocop_process(file_path) - result[:error_count] = rubocop_result.count - result[:error_messages] = rubocop_result.map(&:message) - end - result - end - - private - - def self.rubocop_process(file_path) - process_source = RuboCop::ProcessedSource.from_file(file_path, ruby_version) - config = RuboCop::ConfigStore.new.for(process_source.path) - cop_classes = RuboCop::Cop::Cop.all - registry = RuboCop::Cop::Registry.new(cop_classes) - team = RuboCop::Cop::Team.new(registry, config) - team.inspect_file(process_source) - end - - def self.ruby_version - RUBY_VERSION.to_f - end - - def self.ruby_file?(file_path) - File.extname(file_path) == '.rb' - end - end - end -end \ No newline at end of file diff --git a/app/domain/models/contributions/mappers/complexity_mapper.rb b/app/domain/models/contributions/mappers/complexity_mapper.rb index 70ec840..4977e3a 100644 --- a/app/domain/models/contributions/mappers/complexity_mapper.rb +++ b/app/domain/models/contributions/mappers/complexity_mapper.rb @@ -22,8 +22,8 @@ def build_entity private def abc_metric - abc_metric = CodePraise::Complexity::AbcMetric.new(@file_path) - CodePraise::Complexity::Calculator.new(abc_metric).calculate + flog = CodePraise::Complexity::FlogReporter.new(@file_path) + flog.report end end end diff --git a/app/domain/models/contributions/mappers/file_contributions_mapper.rb b/app/domain/models/contributions/mappers/file_contributions_mapper.rb index 757a730..502d3ae 100644 --- a/app/domain/models/contributions/mappers/file_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/file_contributions_mapper.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true -require_relative 'complexity_mapper' -require_relative '../lib/measurement/idiomaticity' - module CodePraise module Mapper # Summarizes a single file's contributions by team members class FileContributions - def initialize(file_report, repo_path) + def initialize(file_report, repo_path, idiomaticity_mapper) @file_report = file_report @repo_path = repo_path + @idiomaticity_mapper = idiomaticity_mapper end def build_entity @@ -38,11 +36,7 @@ def complexity def idiomaticity file_path = Value::FilePath.new(filename) - idiomaticity_hash = Measurement::Idiomaticity.calculate("#{@repo_path}/#{file_path}") - Entity::Idiomaticity.new( - error_count: idiomaticity_hash[:error_count], - error_messages: idiomaticity_hash[:error_messages] - ) + @idiomaticity_mapper.build_entity(file_path) end def summarize_line_reports(line_reports) diff --git a/app/domain/models/contributions/mappers/folder_contributions_mapper.rb b/app/domain/models/contributions/mappers/folder_contributions_mapper.rb index d00f487..6582872 100644 --- a/app/domain/models/contributions/mappers/folder_contributions_mapper.rb +++ b/app/domain/models/contributions/mappers/folder_contributions_mapper.rb @@ -14,6 +14,7 @@ def initialize(folder_name, contributions_reports, repo_path) @folder_name = folder_name @contributions_reports = contributions_reports @repo_path = repo_path + @idiomaticity_mapper = Mapper::Idiomaticity.new(repo_path) end def build_entity @@ -26,7 +27,8 @@ def build_entity def file_summaries @contributions_reports.map do |file_report| - Mapper::FileContributions.new(file_report, @repo_path).build_entity + Mapper::FileContributions.new(file_report, @repo_path, + @idiomaticity_mapper).build_entity end end diff --git a/app/domain/models/contributions/mappers/idiomaticity_mapper.rb b/app/domain/models/contributions/mappers/idiomaticity_mapper.rb new file mode 100644 index 0000000..ce98883 --- /dev/null +++ b/app/domain/models/contributions/mappers/idiomaticity_mapper.rb @@ -0,0 +1,31 @@ +module CodePraise + module Mapper + class Idiomaticity + def initialize(git_repo_path) + @rubocop_reporter = Rubocop::Reporter.new(git_repo_path) + end + + def build_entity(file_path) + Entity::Idiomaticity.new( + offenses: offenses(file_path) + ) + end + + private + + def offenses(file_path) + idiomaticity_result = @rubocop_reporter.report[file_path] + + return nil if idiomaticity_result.nil? + + idiomaticity_result.map do |error_hash| + Entity::Offense.new( + type: error_hash['cop_name'], + message: error_hash['message'], + location: error_hash['location']['start_line'] + ) + end + end + end + end +end diff --git a/app/domain/models/contributions/mappers/method_contribution_mapper.rb b/app/domain/models/contributions/mappers/method_contribution_mapper.rb index 41b4e1a..a59a549 100644 --- a/app/domain/models/contributions/mappers/method_contribution_mapper.rb +++ b/app/domain/models/contributions/mappers/method_contribution_mapper.rb @@ -8,7 +8,11 @@ def initialize(file_contributions) end def build_entity - all_methods.map do |method| + methods = all_methods + + return nil if methods.nil? + + methods.map do |method| Entity::MethodContribution.new( name: method[:name], lines: method[:lines] diff --git a/app/domain/models/contributions/mappers/method_parser.rb b/app/domain/models/contributions/mappers/method_parser.rb index be80945..acd1bd1 100644 --- a/app/domain/models/contributions/mappers/method_parser.rb +++ b/app/domain/models/contributions/mappers/method_parser.rb @@ -19,40 +19,57 @@ def self.line_of_code(line_entities) def self.all_methods_hash(ast, line_entities) methods_ast = [] find_methods_tree(ast, methods_ast) + + return nil if methods_ast.empty? + methods_ast.inject([]) do |result, method_ast| result.push(name: method_name(method_ast), - lines: select_entity(method_ast, line_entities)) + lines: select_entities(method_ast, line_entities)) end end - def self.select_entity(method_ast, line_entities) - method_loc = Unparser.unparse(method_ast).split("\n") - first_number = line_entities.select {|loc| loc.code.strip == method_loc[0].strip}[0].number - end_number = first_number + method_loc.count - 1 - result = [] - while first_number <= end_number - loc = line_entities.select do |line_entity| - line_entity.number == first_number - end.first - first_number += 1 - result.push(loc) - end_number += 1 if blank_line?(loc) || comment?(loc) + def self.select_entities(method_ast, line_entities) + method_name = method_name(method_ast) + end_amount = count_end(method_ast) + + number = line_entities.select do |line_entity| + line_entity.code.include?("def #{method_name}") + end.first.number + + method_entities = [] + + while end_amount.positive? + line_entity = select_entity(line_entities, number) + + break if line_entity.nil? + + end_amount -= 1 if end_entity?(line_entity) + number += 1 + method_entities << line_entity end - result + + method_entities end private - def self.blank_line?(loc) - loc.code.strip.empty? + def self.select_entity(line_entities, number) + line_entities.select do |line_entity| + line_entity.number == number + end.first + end + + def self.end_entity?(line_entity) + line_entity.code.strip == 'end' end - def self.comment?(loc) - loc.code.strip[0] == '#' + def self.count_end(method_ast) + method_lines = Unparser.unparse(method_ast) + method_lines.scan(/end/).count end def self.method_name(method_ast) - method_ast.children[0] + method_ast.children[0].to_s end def self.find_methods_tree(ast, methods_ast) @@ -67,8 +84,8 @@ def self.find_methods_tree(ast, methods_ast) end end - private_class_method :find_methods_tree, :comment?, :blank_line?, - :method_name + private_class_method :find_methods_tree, :count_end, + :method_name, :select_entity, :end_entity? end end end diff --git a/app/domain/models/contributions/values/file_path.rb b/app/domain/models/contributions/values/file_path.rb index a6cd241..61d2bc9 100644 --- a/app/domain/models/contributions/values/file_path.rb +++ b/app/domain/models/contributions/values/file_path.rb @@ -16,7 +16,7 @@ def initialize(filepath) def folder_after(root) raise(ArgumentError, 'Path mismatch') unless - self.start_with?(root) || root.empty? + self.start_with?(root) || root.empty? matches = self.match(%r{(?^#{root}[^\/]+)[\/]?}) matches[:folder] diff --git a/app/infrastructure/complexity/complexity.rb b/app/infrastructure/complexity/complexity.rb deleted file mode 100644 index 05cc557..0000000 --- a/app/infrastructure/complexity/complexity.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - module Complexity - # Complexity Calculator - class Calculator - def initialize(adapter) - @adapter = adapter - end - - def calculate - @adapter.calculate - end - end - end -end diff --git a/app/infrastructure/complexity/abc_metric.rb b/app/infrastructure/flog/flog.rb similarity index 95% rename from app/infrastructure/complexity/abc_metric.rb rename to app/infrastructure/flog/flog.rb index 28e7797..fb664c8 100644 --- a/app/infrastructure/complexity/abc_metric.rb +++ b/app/infrastructure/flog/flog.rb @@ -5,12 +5,12 @@ module CodePraise module Complexity # ABC Metric calculation class - class AbcMetric + class FlogReporter def initialize(file_path) @file_path = file_path end - def calculate + def report return nil unless ruby_file? flog_result = flog_process diff --git a/app/infrastructure/complexity/init.rb b/app/infrastructure/flog/init.rb similarity index 100% rename from app/infrastructure/complexity/init.rb rename to app/infrastructure/flog/init.rb diff --git a/app/infrastructure/init.rb b/app/infrastructure/init.rb index 0e004fe..80bf989 100644 --- a/app/infrastructure/init.rb +++ b/app/infrastructure/init.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -folders = %w[github database git cache messaging complexity] +folders = %w[github database git cache messaging flog rubocop] folders.each do |folder| require_relative "#{folder}/init.rb" end diff --git a/app/infrastructure/rubocop/command.rb b/app/infrastructure/rubocop/command.rb new file mode 100644 index 0000000..f4ecdd5 --- /dev/null +++ b/app/infrastructure/rubocop/command.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module CodePraise + module Rubocop + # Use object to operate rubocop command + class Command + FORMAT = { + 'json' => 'j' + }.freeze + + RUBOCOP = 'rubocop' + + def initialize + @format = '' + @except = [] + @redirects = [] + @target = '' + end + + def format(output_format) + @format = FORMAT[output_format] + self + end + + def target(file_path) + @target = file_path == '/' ? '' : file_path + self + end + + def except(cop) + @except << cop + self + end + + def with_std_error + @redirects << '2>&1' + self + end + + def full_command + [RUBOCOP, @target, options, @redirects].join(' ') + end + + private + + def options + "--except #{@except.join(',')} -f #{@format}" + end + end + end +end diff --git a/app/infrastructure/rubocop/init.rb b/app/infrastructure/rubocop/init.rb new file mode 100644 index 0000000..50d81c0 --- /dev/null +++ b/app/infrastructure/rubocop/init.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +Dir.glob("#{File.dirname(__FILE__)}/*.rb").each do |file| + require file +end diff --git a/app/infrastructure/rubocop/reporter.rb b/app/infrastructure/rubocop/reporter.rb new file mode 100644 index 0000000..d0857f5 --- /dev/null +++ b/app/infrastructure/rubocop/reporter.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative 'command' + +module CodePraise + module Rubocop + class Reporter + def initialize(git_repo_path, target = '') + @git_repo_path = git_repo_path + @command = Command.new + .target(target) + .except('Metrics') + .format('json') + .with_std_error + end + + def report + @report ||= JSON.parse(call)['files'].each_with_object({}) do |file, hash| + hash[file['path']] = file['offenses'] + end + end + + private + + def call + in_repo do + `#{@command.full_command}` + end + end + + def in_repo(&block) + Dir.chdir(@git_repo_path) { yield block } + end + end + end +end diff --git a/app/presentation/representers/idiomaticity_representer.rb b/app/presentation/representers/idiomaticity_representer.rb index 8f191be..0fad0c4 100644 --- a/app/presentation/representers/idiomaticity_representer.rb +++ b/app/presentation/representers/idiomaticity_representer.rb @@ -1,14 +1,17 @@ +# frozen_string_literal: true + require 'roar/decorator' require 'roar/json' +require_relative 'offense_representer' + module CodePraise module Representer - # Represents folder summary about repo's folder + # Represents Idiomaticity Errors class Idiomaticity < Roar::Decorator include Roar::JSON - property :error_count - collection :error_messages + collection :offenses, extend: Representer::Offense, class: OpenStruct end end end diff --git a/app/presentation/representers/offense_representer.rb b/app/presentation/representers/offense_representer.rb new file mode 100644 index 0000000..41b33e5 --- /dev/null +++ b/app/presentation/representers/offense_representer.rb @@ -0,0 +1,17 @@ +# fronze_string_literal: true + +require 'roar/decorator' +require 'roar/json' + +module CodePraise + module Representer + # Represent idiomaticity offense information + class Offense < Roar::Decorator + include Roar::JSON + + property :type + property :message + property :location + end + end +end \ No newline at end of file diff --git a/lib/math_extension.rb b/lib/math_extension.rb index 609311e..853cd6c 100644 --- a/lib/math_extension.rb +++ b/lib/math_extension.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true +# Math module Math def self.coefficient_variation(nums) + return nil if mean(nums).zero? + (sigma(nums) / mean(nums) * 100).round end @@ -10,6 +13,8 @@ def self.sigma(nums) end def self.variance(nums) + return nil if nums.count.zero? + m = mean(nums) sum = 0.0 nums.each {|v| sum += (v-m)**2 } @@ -17,6 +22,8 @@ def self.variance(nums) end def self.mean(nums) + return nil if nums.count.zero? + nums.reduce(:+).to_f / nums.count end end \ No newline at end of file diff --git a/spec/factories/project_factory.rb b/spec/factories/project_factory.rb index 128fca3..4110724 100644 --- a/spec/factories/project_factory.rb +++ b/spec/factories/project_factory.rb @@ -2,13 +2,13 @@ FactoryBot.define do factory :project, class: "CodePraise::Database::ProjectOrm" do - origin_id { 126318191 } - name {"alpha-blog"} - size { 7181 } - ssh_url { "git://github.com/XuVic/alpha-blog.git" } - http_url { "https://github.com/XuVic/alpha-blog" } + origin_id { 131723249 } + name {"talkup_api"} + size { 168 } + ssh_url { "git@github.com:PigAndChicken/talkup_api.git" } + http_url { "https://github.com/PigAndChicken/talkup_api.git" } association :owner, factory: :member - initialize_with { CodePraise::Database::ProjectOrm.find(origin_id: 126318191) || CodePraise::Database::ProjectOrm.create(attributes) } + initialize_with { CodePraise::Database::ProjectOrm.find(origin_id: 131723249) || CodePraise::Database::ProjectOrm.create(attributes) } factory :project_with_contributor do after(:create) do |project| diff --git a/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb index 22729c4..d742196 100644 --- a/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb +++ b/spec/tests_integration/measurement_integration/commit_level_metrics_spec.rb @@ -1,7 +1,8 @@ -require_relative '../../helpers/spec_helper.rb' +# frozen_string_literal: true -describe "Test Commit-Level Measurement" do +require_relative '../../helpers/spec_helper.rb' +describe 'Test Commit-Level Measurement' do before do project = create(:project) git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) @@ -9,8 +10,8 @@ @commits = contributions.commits end - describe "Entity::Commit" do - it "should return commit information" do + describe 'Entity::Commit' do + it 'should return commit information' do commit = @commits[0] _(@commits.size).must_be :>, 0 _(commit.total_additions).must_be :>, 0 @@ -19,8 +20,8 @@ end end - describe "Entity::FileChange" do - it "should return addition, deletion and file information" do + describe 'Entity::FileChange' do + it 'should return addition, deletion and file information' do file_change = @commits[0].file_changes[0] _(file_change.addition).wont_be_nil _(file_change.deletion).wont_be_nil diff --git a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb index 237cfb2..ea56eb5 100644 --- a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb +++ b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb @@ -1,48 +1,20 @@ -require_relative '../../helpers/spec_helper.rb' +# frozen_string_literal: true -describe "Test File-Level Measurement" do +require_relative '../../helpers/spec_helper.rb' - before do +describe 'Test File-Level Measurement' do + before(:all) do project = create(:project) git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) contributions = CodePraise::Mapper::Contributions.new(git_repo) - @folder = contributions.for_folder('app') - @file = @folder.files[0] - binding.pry + @folder = contributions.for_folder('') + @file = @folder.files[35] end - describe "Entity::Complexity" do - it "should return complexity score" do + describe 'Entity::Complexity' do + it 'should return complexity score' do _(@file.complexity.average).wont_be_nil - _(@file.complexity.methods_complexity.keys).wont_be_empty - end - end - - describe "Entity::Idiomaticity" do - it "should count number of unidiomatic code" do - _(@file.idiomaticity.error_count).wont_be_nil - _(@file.idiomaticity.error_messages).wont_be_empty - end - end - - describe "Annotation" do - it "should return number of annotation" do - _(@file.total_annotations).wont_be_nil + _(@file.complexity.methods.keys).wont_be_empty end end - - describe "Entity::MethodContribution" do - it "should return number of method" do - methods = @file.methods - _(methods.count).wont_be_nil - _(methods[0].lines).wont_be_nil - end - end - - describe "Entity::CollectiveOwnership" do - it "should return personal code ownership" do - _(@folder.collective_ownership.keys).wont_be_empty - end - end - -end \ No newline at end of file +end diff --git a/spec/tests_unit/rubocop_spec.rb b/spec/tests_unit/rubocop_spec.rb new file mode 100644 index 0000000..5642f50 --- /dev/null +++ b/spec/tests_unit/rubocop_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative '../helpers/spec_helper.rb' + +JSON_FORMAT_COMMAND = 'rubocop --except Metrics -f j 2>&1' +REPO_PATH = 'app/infrastructure/git/repostore/znjWpkQzvSU8ZnQ82oXCbLVIO6X5L69XkZuDuN6aKaw=' +FILE_PATH = 'Gemfile' + +describe 'Rubucop Module Unit Test' do + describe CodePraise::Rubocop::Command do + it 'should make right rubocop command' do + command = CodePraise::Rubocop::Command.new + .target('/') + .except('Metrics') + .format('json') + .with_std_error + .full_command + + _(command).must_equal JSON_FORMAT_COMMAND + end + end + + describe CodePraise::Rubocop::Reporter do + it 'should return rubcop result with hash format' do + rubocop_reporter = CodePraise::Rubocop::Reporter.new(REPO_PATH) + _(rubocop_reporter.report).must_be_kind_of Hash + _(rubocop_reporter.report.keys).wont_be_empty + _(rubocop_reporter.report[FILE_PATH]).wont_be_nil + end + end +end diff --git a/workers/shoryuken_dev.yml b/workers/shoryuken_dev.yml index 9d7d62a..e3a5e5f 100644 --- a/workers/shoryuken_dev.yml +++ b/workers/shoryuken_dev.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.ap-northeast-1.amazonaws.com/503315808870/codepraise-clone-development + - https://sqs.us-east-2.amazonaws.com/700371359912/codepraise-clone-development diff --git a/workers/shoryuken_test.yml b/workers/shoryuken_test.yml index ba59b11..e881152 100644 --- a/workers/shoryuken_test.yml +++ b/workers/shoryuken_test.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.ap-northeast-1.amazonaws.com/503315808870/codepraise-clone-test + - https://sqs.us-east-2.amazonaws.com/700371359912/codepraise-clone-test From 5e3c6ac1355c0426d9e2b47d581c1cecdcb48bb4 Mon Sep 17 00:00:00 2001 From: vicxu Date: Mon, 8 Apr 2019 14:30:44 +0800 Subject: [PATCH 12/14] rename attributes in filecontribution entity --- .../entities/children/file_contributions.rb | 8 +++--- .../entities/root/folder_contributions.rb | 18 ++++++++---- .../lib/code_ownership_calculator.rb | 2 +- .../contributions/lib/comment_calculator.rb | 28 +++++++++++-------- app/infrastructure/rubocop/command.rb | 2 +- app/infrastructure/rubocop/reporter.rb | 2 +- .../file_contributions_representer.rb | 8 +++--- .../folder_contributions_representer.rb | 5 ++-- .../file_level_metrics_spec.rb | 13 ++++++++- spec/tests_unit/rubocop_spec.rb | 2 +- 10 files changed, 57 insertions(+), 31 deletions(-) diff --git a/app/domain/models/contributions/entities/children/file_contributions.rb b/app/domain/models/contributions/entities/children/file_contributions.rb index fb2bd66..a471ecd 100644 --- a/app/domain/models/contributions/entities/children/file_contributions.rb +++ b/app/domain/models/contributions/entities/children/file_contributions.rb @@ -22,11 +22,11 @@ def initialize(file_path:, lines:, complexity:, idiomaticity:, methods:) @methods = methods end - def total_credits - credit_share.total_credits + def total_line_credits + line_credit_share.total_credits end - def credit_share + def line_credit_share return Value::CreditShare.new if not_wanted @credit_share ||= lines @@ -36,7 +36,7 @@ def credit_share end def contributors - credit_share.contributors + line_credit_share.contributors end private diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index ca05d72..114d182 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -24,14 +24,22 @@ def line_count files.map(&:line_count).reduce(&:+) end - def total_credits - files.map(&:total_credits).sum + def total_line_credits + files.map(&:total_line_credits).sum end def lines files.map(&:lines).reduce(&:+) end + def average_complexity + files_complexity = files.map(&:complexity).reject(&:nil?) + + return 0 if files_complexity.count.zero? + + files_complexity.map(&:average).reduce(:+) / files_complexity.count + end + def base_files @base_files ||= files.select do |file| file.file_path.directory == comparitive_path @@ -60,12 +68,12 @@ def any_base_files? base_files.count.positive? end - def credit_share - @credit_share ||= files.map(&:credit_share).reduce(&:+) + def line_credit_share + @credit_share ||= files.map(&:line_credit_share).reduce(&:+) end def contributors - credit_share.contributors + line_credit_share.contributors end private diff --git a/app/domain/models/contributions/lib/code_ownership_calculator.rb b/app/domain/models/contributions/lib/code_ownership_calculator.rb index fba62fc..4c65af5 100644 --- a/app/domain/models/contributions/lib/code_ownership_calculator.rb +++ b/app/domain/models/contributions/lib/code_ownership_calculator.rb @@ -19,7 +19,7 @@ def coefficient_variation def contributros_subfolders_percentage percentage_array = subfolders.map do |subfolder| - subfolder.credit_share.share_percentage + subfolder.line_credit_share.share_percentage end contributors_hash = contributors.each_with_object({}) {|contributor, hash| hash[contributor.username] = []; hash} percentage_array.each do |subfolder_percentage| diff --git a/app/domain/models/contributions/lib/comment_calculator.rb b/app/domain/models/contributions/lib/comment_calculator.rb index 978d4ef..8783218 100644 --- a/app/domain/models/contributions/lib/comment_calculator.rb +++ b/app/domain/models/contributions/lib/comment_calculator.rb @@ -7,32 +7,38 @@ module CommentCalculator MULTILINE = 2 COMMENT = '#' - def multiline_comments + def multiline_comment_count + count_comments(:multiline_comment?) + end + + def singleline_comment_count + count_comments(:singleline_comment?) + end + + private + + def count_comments(condition) comment_index = not_comment_index = documentation = 0 lines_of_code.each_with_index do |loc, i| if comment?(loc) comment_index = i else - documentation += 1 if multiline_comment?(comment_index, not_comment_index) + documentation += 1 if method(condition).call(comment_index, not_comment_index) not_comment_index = i end end - documentation += 1 if multiline_comment?(comment_index, not_comment_index) + documentation += 1 if method(condition).call(comment_index, not_comment_index) documentation end - def comments - lines_of_code.select do |loc| - comment?(loc) - end.count - end - - private - def lines_of_code lines.map(&:code) end + def singleline_comment?(comment_index, not_comment_index) + (comment_index - not_comment_index) == 1 + end + def multiline_comment?(comment_index, not_comment_index) (comment_index - not_comment_index) >= MULTILINE end diff --git a/app/infrastructure/rubocop/command.rb b/app/infrastructure/rubocop/command.rb index f4ecdd5..9885bd2 100644 --- a/app/infrastructure/rubocop/command.rb +++ b/app/infrastructure/rubocop/command.rb @@ -32,7 +32,7 @@ def except(cop) self end - def with_std_error + def with_stderr_output @redirects << '2>&1' self end diff --git a/app/infrastructure/rubocop/reporter.rb b/app/infrastructure/rubocop/reporter.rb index d0857f5..b9016f6 100644 --- a/app/infrastructure/rubocop/reporter.rb +++ b/app/infrastructure/rubocop/reporter.rb @@ -11,7 +11,7 @@ def initialize(git_repo_path, target = '') .target(target) .except('Metrics') .format('json') - .with_std_error + .with_stderr_output end def report diff --git a/app/presentation/representers/file_contributions_representer.rb b/app/presentation/representers/file_contributions_representer.rb index 0d67fc1..a72183d 100644 --- a/app/presentation/representers/file_contributions_representer.rb +++ b/app/presentation/representers/file_contributions_representer.rb @@ -18,13 +18,13 @@ class FileContributions < Roar::Decorator include Roar::JSON property :line_count - property :total_credits + property :total_line_credits property :total_methods - property :multiline_comments - property :comments + property :multiline_comment_count + property :singleline_comment_count collection :methods, extend: Representer::MethodContributions, class: OpenStruct property :file_path, extend: Representer::FilePath, class: OpenStruct - property :credit_share, extend: Representer::CreditShare, class: OpenStruct + property :line_credit_share, extend: Representer::CreditShare, class: OpenStruct property :complexity, extend: Representer::Complexity, class: OpenStruct property :idiomaticity, extend: Representer::Idiomaticity, class: OpenStruct collection :contributors, extend: Representer::Contributor, class: OpenStruct diff --git a/app/presentation/representers/folder_contributions_representer.rb b/app/presentation/representers/folder_contributions_representer.rb index a9a9f9e..5fbd333 100644 --- a/app/presentation/representers/folder_contributions_representer.rb +++ b/app/presentation/representers/folder_contributions_representer.rb @@ -16,11 +16,12 @@ class FolderContributions < Roar::Decorator property :path property :line_count - property :total_credits + property :total_line_credits property :any_subfolders? property :any_base_files? property :coefficient_variation - property :credit_share, extend: Representer::CreditShare, class: OpenStruct + property :average_complexity + property :line_credit_share, extend: Representer::CreditShare, class: OpenStruct collection :base_files, extend: Representer::FileContributions, class: OpenStruct collection :subfolders, extend: Representer::FolderContributions, class: OpenStruct collection :contributors, extend: Representer::Contributor, class: OpenStruct diff --git a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb index ea56eb5..e1c5297 100644 --- a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb +++ b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb @@ -3,12 +3,13 @@ require_relative '../../helpers/spec_helper.rb' describe 'Test File-Level Measurement' do - before(:all) do + before do project = create(:project) git_repo = CodePraise::GitRepo.new(project, CodePraise::Api.config) contributions = CodePraise::Mapper::Contributions.new(git_repo) @folder = contributions.for_folder('') @file = @folder.files[35] + # binding.pry end describe 'Entity::Complexity' do @@ -17,4 +18,14 @@ _(@file.complexity.methods.keys).wont_be_empty end end + + describe 'Mixins::CommentCalculator' do + it 'should count the multiline comment' do + _(@file.multiline_comment_count).must_equal 1 + end + + it 'should count the signle line comment' do + _(@file.singleline_comment_count).must_equal 1 + end + end end diff --git a/spec/tests_unit/rubocop_spec.rb b/spec/tests_unit/rubocop_spec.rb index 5642f50..1b1c823 100644 --- a/spec/tests_unit/rubocop_spec.rb +++ b/spec/tests_unit/rubocop_spec.rb @@ -13,7 +13,7 @@ .target('/') .except('Metrics') .format('json') - .with_std_error + .with_stderr_output .full_command _(command).must_equal JSON_FORMAT_COMMAND From 90ebea2698af8a9bbf7c076f7204a50bca74fafb Mon Sep 17 00:00:00 2001 From: vicxu Date: Thu, 11 Apr 2019 13:09:09 +0800 Subject: [PATCH 13/14] add some test into integration spec --- .../entities/children/method_contribution.rb | 1 - .../models/contributions/entities/root/commit.rb | 4 +--- .../entities/root/folder_contributions.rb | 4 ++++ .../models/contributions/lib/comment_calculator.rb | 12 ++++++------ spec/helpers/comment_calculator_spec.rb | 7 +++++++ .../file_level_metrics_spec.rb | 12 +++++++++--- 6 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 spec/helpers/comment_calculator_spec.rb diff --git a/app/domain/models/contributions/entities/children/method_contribution.rb b/app/domain/models/contributions/entities/children/method_contribution.rb index 5053ef2..f0cfecf 100644 --- a/app/domain/models/contributions/entities/children/method_contribution.rb +++ b/app/domain/models/contributions/entities/children/method_contribution.rb @@ -8,7 +8,6 @@ module CodePraise module Entity # Entity for a single method contributed by a team-member class MethodContribution < Dry::Struct - include Dry::Types.module attribute :name, Coercible::String diff --git a/app/domain/models/contributions/entities/root/commit.rb b/app/domain/models/contributions/entities/root/commit.rb index f9df887..d184733 100644 --- a/app/domain/models/contributions/entities/root/commit.rb +++ b/app/domain/models/contributions/entities/root/commit.rb @@ -4,15 +4,13 @@ require 'dry-struct' require_relative '../children/contributor' - module CodePraise module Entity # Entity for a single line of code contributed by a team-member class Commit < Dry::Struct - include Dry::Types.module - attribute :committer, Contributor + attribute :committer, Contributor attribute :sha, Strict::String attribute :date, Params::DateTime attribute :size, Strict::Integer diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index 114d182..58ded75 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -40,6 +40,10 @@ def average_complexity files_complexity.map(&:average).reduce(:+) / files_complexity.count end + def total_offenses + + end + def base_files @base_files ||= files.select do |file| file.file_path.directory == comparitive_path diff --git a/app/domain/models/contributions/lib/comment_calculator.rb b/app/domain/models/contributions/lib/comment_calculator.rb index 8783218..2ad64dc 100644 --- a/app/domain/models/contributions/lib/comment_calculator.rb +++ b/app/domain/models/contributions/lib/comment_calculator.rb @@ -8,17 +8,15 @@ module CommentCalculator COMMENT = '#' def multiline_comment_count - count_comments(:multiline_comment?) + count_comments(:multiline_comment?, lines_of_code) end def singleline_comment_count - count_comments(:singleline_comment?) + count_comments(:singleline_comment?, lines_of_code) end - private - - def count_comments(condition) - comment_index = not_comment_index = documentation = 0 + def count_comments(condition, lines_of_code) + comment_index = not_comment_index = documentation = -1 lines_of_code.each_with_index do |loc, i| if comment?(loc) comment_index = i @@ -31,6 +29,8 @@ def count_comments(condition) documentation end + private + def lines_of_code lines.map(&:code) end diff --git a/spec/helpers/comment_calculator_spec.rb b/spec/helpers/comment_calculator_spec.rb new file mode 100644 index 0000000..3abb733 --- /dev/null +++ b/spec/helpers/comment_calculator_spec.rb @@ -0,0 +1,7 @@ +class CommentTest + include Mixins::CommentCalcualtor + + def initialize(loc) + @loc = loc + end +end \ No newline at end of file diff --git a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb index e1c5297..d8681d9 100644 --- a/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb +++ b/spec/tests_integration/measurement_integration/file_level_metrics_spec.rb @@ -9,17 +9,23 @@ contributions = CodePraise::Mapper::Contributions.new(git_repo) @folder = contributions.for_folder('') @file = @folder.files[35] - # binding.pry end - describe 'Entity::Complexity' do + describe CodePraise::Entity::Complexity do it 'should return complexity score' do _(@file.complexity.average).wont_be_nil _(@file.complexity.methods.keys).wont_be_empty end end - describe 'Mixins::CommentCalculator' do + describe CodePraise::Entity::MethodContribution do + it 'should count number of method correctly' do + _(@file.methods.count).must_equal 2 + _(@file.methods.map { |method| method.lines.count }).must_equal [3, 3] + end + end + + describe CodePraise::Mixins::CommentCalculator do it 'should count the multiline comment' do _(@file.multiline_comment_count).must_equal 1 end From af7a5ef9d4b8a8effabc2e1e8eaf24d0903ed7dc Mon Sep 17 00:00:00 2001 From: vicxu Date: Thu, 11 Apr 2019 13:10:19 +0800 Subject: [PATCH 14/14] rm redundant file --- .../contributions/entities/root/folder_contributions.rb | 4 ---- spec/helpers/comment_calculator_spec.rb | 7 ------- 2 files changed, 11 deletions(-) delete mode 100644 spec/helpers/comment_calculator_spec.rb diff --git a/app/domain/models/contributions/entities/root/folder_contributions.rb b/app/domain/models/contributions/entities/root/folder_contributions.rb index 58ded75..114d182 100644 --- a/app/domain/models/contributions/entities/root/folder_contributions.rb +++ b/app/domain/models/contributions/entities/root/folder_contributions.rb @@ -40,10 +40,6 @@ def average_complexity files_complexity.map(&:average).reduce(:+) / files_complexity.count end - def total_offenses - - end - def base_files @base_files ||= files.select do |file| file.file_path.directory == comparitive_path diff --git a/spec/helpers/comment_calculator_spec.rb b/spec/helpers/comment_calculator_spec.rb deleted file mode 100644 index 3abb733..0000000 --- a/spec/helpers/comment_calculator_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -class CommentTest - include Mixins::CommentCalcualtor - - def initialize(loc) - @loc = loc - end -end \ No newline at end of file