├── .eslintrc.js ├── .github └── workflows │ └── ci-tests-workflow.yaml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── jest.config.js ├── lib ├── analysers │ ├── code_maat │ │ ├── Dockerfile │ │ ├── code-maat-1.0.1-standalone.jar │ │ ├── code_maat_analyser.js │ │ ├── docker_command_definition.js │ │ ├── index.js │ │ └── java_command_definition.js │ ├── escomplex │ │ ├── escomplex_analyser.js │ │ └── index.js │ ├── flog │ │ ├── flog_analyser.js │ │ ├── flog_parser.js │ │ └── index.js │ ├── sloc │ │ ├── index.js │ │ └── sloc_analyser.js │ └── word_count │ │ ├── index.js │ │ ├── text_filters.js │ │ ├── word_count_analyser.js │ │ └── word_counter.js ├── api_middleware │ ├── index.js │ └── load_reports.js ├── command │ ├── command.js │ └── index.js ├── graph_support │ ├── index.js │ ├── tree.js │ ├── tree_node.js │ ├── weighted_collection.js │ └── weighted_tree.js ├── index.js ├── log │ └── index.js ├── models │ ├── developers_info.js │ ├── index.js │ ├── language_definitions.js │ ├── layer_grouping.js │ ├── repository.js │ ├── repository_path.js │ ├── task │ │ ├── gulp_task.js │ │ ├── runners │ │ │ ├── default_runner.js │ │ │ ├── index.js │ │ │ └── report_runner.js │ │ └── task_definitions.js │ └── time_interval │ │ ├── builder.js │ │ ├── time_period.js │ │ └── time_splitter.js ├── parallel_processing │ ├── index.js │ ├── job_scheduler.js │ ├── stream_processor.js │ └── task_executor.js ├── reporting │ ├── data_source_handler.js │ ├── index.js │ ├── merge_strategies.js │ ├── object_transformer.js │ ├── publisher.js │ └── report_composer.js ├── runtime │ ├── app_config.js │ ├── defaults.js │ ├── env_config_reader.js │ ├── errors.js │ ├── file_config_reader.js │ └── task_context.js ├── tasks │ ├── code_analysis_tasks.js │ ├── code_maat_reports_tasks.js │ ├── complexity_analysis │ │ ├── javascript_tasks.js │ │ └── ruby_tasks.js │ ├── coupling_analysis_tasks.js │ ├── helpers │ │ ├── code_maat_helper.js │ │ ├── developer_data_helper.js │ │ ├── files_helper.js │ │ ├── graph_data_helper.js │ │ ├── index.js │ │ └── revision_helper.js │ ├── hotspot_analysis_tasks.js │ ├── misc_tasks.js │ ├── social_analysis_tasks.js │ ├── system_analysis │ │ ├── churn_metrics.js │ │ ├── coupling_metrics.js │ │ ├── data_collector.js │ │ ├── metric_collection_strategies │ │ │ ├── index.js │ │ │ ├── multi_layer_strategy.js │ │ │ ├── no_layer_strategy.js │ │ │ ├── split_layer_strategy.js │ │ │ └── time_period_results.js │ │ ├── summary_metrics.js │ │ └── system_analysis_tasks.js │ └── vcs_tasks.js ├── utils │ ├── arrays.js │ ├── definitions_archive.js │ ├── file_system.js │ ├── index.js │ ├── json.js │ ├── path_matchers.js │ ├── platform_check.js │ ├── require_ifexists.js │ ├── singleton_factory.js │ ├── stream.js │ └── xml_utils.js ├── vcs │ ├── git │ │ ├── git_adapter.js │ │ ├── gitlog_stream_transformer.js │ │ └── index.js │ ├── index.js │ ├── svn │ │ ├── index.js │ │ ├── svn_adapter.js │ │ └── svnlog_stream_transformer.js │ └── vcs_factory.js └── web │ ├── bootstrap.js │ ├── controllers │ └── report_controller.js │ ├── d3_chart_components │ ├── d3_axis.js │ ├── d3_brush.js │ ├── d3_chart.js │ ├── d3_component.js │ ├── d3_data.js │ ├── d3_element.js │ ├── d3_node.js │ ├── d3_tooltip.js │ ├── d3_transform.js │ └── d3_zoom.js │ ├── diagrams │ ├── bar_chart │ │ ├── multibar_chart_model.js │ │ └── vertical_bar_chart_model.js │ ├── bubble_chart │ │ ├── circle_select_handler.js │ │ ├── diagram_model.js │ │ ├── node_helper.js │ │ └── pack_layout_adapter.js │ ├── controls_proxy.js │ ├── data_proxy.js │ ├── diagram.js │ ├── enclosure_chart │ │ ├── base_node_helper.js │ │ ├── clipboard_handler.js │ │ ├── colored_diagram_model.js │ │ ├── colored_node_helper.js │ │ ├── pack_layout_adapter.js │ │ ├── weighted_diagram_model.js │ │ ├── weighted_node_helper.js │ │ └── zoom_handler.js │ ├── graph_painter.js │ ├── line_chart │ │ ├── clipboard_handler.js │ │ ├── diagram_model.js │ │ ├── diagram_support.js │ │ ├── legend_model.js │ │ ├── scatter_points_data_model.js │ │ ├── zoom_brush_handler.js │ │ └── zoomable_diagram_model.js │ ├── network_graph │ │ ├── diagram_model.js │ │ ├── force_drag.js │ │ └── force_simulation_handler.js │ ├── treemap │ │ ├── diagram_model.js │ │ ├── node_helper.js │ │ ├── treemap_layout_adapter.js │ │ └── zoom_handler.js │ └── word_cloud │ │ ├── cloud_layout_adapter.js │ │ └── diagram_model.js │ ├── filters │ ├── base_filter.js │ ├── color_range.js │ ├── group_list_selection.js │ ├── index.js │ ├── metric_range.js │ ├── percentage_metric_range.js │ └── regexp.js │ ├── helpers │ ├── clipboard_helper.js │ └── mustache_helper.js │ ├── main.js │ ├── models │ ├── graph_control_group.js │ ├── graph_model.js │ ├── manifest_mixin.js │ ├── report_model.js │ ├── reports_list_model.js │ ├── series_group_model.js │ └── template_register.js │ ├── reports │ ├── complexity_trend.js │ ├── developer_coupling.js │ ├── developer_effort.js │ ├── hotspot_analysis.js │ ├── index.js │ ├── knowledge_map.js │ ├── sloc_trend.js │ ├── sum_of_coupling.js │ ├── system_evolution.js │ ├── temporal_coupling.js │ └── word_cloud.js │ ├── utils │ ├── async_loader.js │ ├── color_scale_factory.js │ ├── locale_detection.js │ ├── query_parameter.js │ ├── rotating_sequence.js │ └── scale_domain_factory.js │ ├── view_model.js │ └── widgets │ ├── color_map.js │ └── index.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── styles │ ├── d3_tip.less │ ├── diagrams.less │ ├── diagrams │ │ ├── bar_chart_diagram.less │ │ ├── circle_packing_diagram.less │ │ ├── line_chart_diagram.less │ │ ├── network_graph_diagram.less │ │ ├── treemap_diagram.less │ │ └── word_cloud_diagram.less │ ├── graph_controls.less │ ├── main.less │ ├── mixins.less │ ├── reports_table.less │ └── slider.less └── templates │ ├── color_map_filters_control_template.html │ ├── color_map_widgets_control_template.html │ ├── element_info_tooltip_template.html │ ├── grouplist_selection_filters_control_template.html │ ├── item_list_tooltip_template.html │ ├── metric_range_filters_control_template.html │ ├── percentage_range_filters_control_template.html │ └── text_filters_control_template.html ├── scripts ├── check-license-header.js ├── check-node-version.js └── webserver.js └── spec ├── analysers ├── code_maat │ ├── code_maat_analyser.spec.js │ ├── docker_command_definition.spec.js │ └── java_command_definition.spec.js ├── escomplex │ └── escomplex_analyser.spec.js ├── flog │ ├── flog_analyser.spec.js │ └── flog_parser.spec.js ├── sloc │ └── sloc_analyser.spec.js └── word_count │ ├── text_filters.spec.js │ ├── word_count_analyser.spec.js │ └── word_counter.spec.js ├── api_middleware ├── index.spec.js └── load_reports.spec.js ├── command └── command.spec.js ├── graph_support ├── tree.spec.js ├── tree_node.spec.js ├── weighted_collection.spec.js └── weighted_tree.spec.js ├── jest_env_setup.js ├── jest_helpers.js ├── jest_tasks_helpers.js ├── log └── logger.spec.js ├── models ├── developers_info.spec.js ├── layer_grouping.spec.js ├── repository.spec.js ├── repository_path.spec.js ├── task │ ├── gulp_task.spec.js │ ├── runners │ │ ├── default_runner.spec.js │ │ └── report_runner.spec.js │ └── task_definitions.spec.js └── time_interval │ ├── builder.spec.js │ ├── time_period.spec.js │ └── time_splitter.spec.js ├── parallel_processing ├── job_scheduler.spec.js ├── stream_processor.spec.js └── task_executor.spec.js ├── reporting ├── data_source_handler.spec.js ├── merge_strategies.spec.js ├── object_transformer.spec.js ├── publisher.spec.js └── report_composer.spec.js ├── runtime ├── app_config.spec.js ├── env_config_reader.spec.js ├── file_config_reader.spec.js └── task_context.spec.js ├── tasks ├── __snapshots__ │ ├── code_analysis_tasks.spec.js.snap │ ├── code_maat_reports_tasks.spec.js.snap │ ├── coupling_analysis_tasks.spec.js.snap │ ├── hotspot_analysis_tasks.spec.js.snap │ ├── misc_tasks.spec.js.snap │ ├── social_analysis_tasks.spec.js.snap │ └── vcs_tasks.spec.js.snap ├── code_analysis_tasks.spec.js ├── code_maat_reports_tasks.spec.js ├── complexity_analysis │ ├── __snapshots__ │ │ ├── javascript_tasks.spec.js.snap │ │ └── ruby_tasks.spec.js.snap │ ├── javascript_tasks.spec.js │ └── ruby_tasks.spec.js ├── coupling_analysis_tasks.spec.js ├── helpers │ ├── code_maat_helper.spec.js │ ├── developer_data_helper.spec.js │ ├── graph_data_helper.spec.js │ └── revision_helper.spec.js ├── hotspot_analysis_tasks.spec.js ├── misc_tasks.spec.js ├── social_analysis_tasks.spec.js ├── system_analysis │ ├── __snapshots__ │ │ └── system_analysis_tasks.spec.js.snap │ ├── churn_metrics.spec.js │ ├── coupling_metrics.spec.js │ ├── data_collector.spec.js │ ├── metric_collection_strategies │ │ ├── multi_layer_strategy.spec.js │ │ ├── no_layer_strategy.spec.js │ │ ├── split_layer_strategy.spec.js │ │ └── time_period_results.spec.js │ ├── summary_metrics.spec.js │ └── system_analysis_tasks.spec.js └── vcs_tasks.spec.js ├── utils ├── arrays.spec.js ├── definitions_archive.spec.js ├── file_system.spec.js ├── json.spec.js ├── path_matchers.spec.js ├── platform_check.spec.js ├── singleton_factory.spec.js ├── stream.spec.js └── xml_utils.spec.js ├── vcs ├── git │ ├── git_adapter.spec.js │ └── gitlog_stream_transformer.spec.js ├── svn │ ├── svn_adapter.spec.js │ └── svnlog_stream_transformer.spec.js ├── vcs.spec.js └── vcs_factory.spec.js └── web ├── controllers └── report_controller.spec.js └── diagrams └── data_proxy.spec.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | plugins: ['jest'], 7 | extends: ['eslint:recommended', 'plugin:jest/recommended', 'plugin:jest/style'], 8 | rules: { 9 | quotes: ['error', 'single', { avoidEscape: true }], 10 | semi: ['error', 'always'], 11 | 'comma-dangle': ['error', 'never'] 12 | }, 13 | overrides: [ 14 | { 15 | files: ['lib/web/**/*.js'], 16 | env: { 17 | browser: true 18 | } 19 | }, 20 | { 21 | files: [ 'spec/**/*.js' ], 22 | env: { 23 | jest: true 24 | }, 25 | rules: { 26 | 'no-console': 'off', 27 | 'jest/valid-describe': 'off' 28 | } 29 | } 30 | ] 31 | }; 32 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - 'dependabot/**' 7 | pull_request: 8 | branches: 9 | - main 10 | jobs: 11 | test_and_lint: 12 | name: Run tests and lint code 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node: [8, 12] 17 | steps: 18 | - uses: actions/checkout@v1 19 | with: 20 | fetch-depth: 1 21 | - run: npm install 22 | - run: npm test 23 | - run: npm run lint 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test_fixtures/ 3 | .DS_Store 4 | .node-version 5 | npm-debug.log 6 | todo.txt 7 | *.tgz 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | var Path = require('path'); 2 | 3 | module.exports = { 4 | testEnvironment: 'node', 5 | modulePaths: ['/lib'], 6 | testMatch: ['/spec/**/?(*.)spec.js'], 7 | clearMocks: true, 8 | errorOnDeprecated: false, 9 | setupFilesAfterEnv: ['/spec/jest_env_setup.js'], 10 | globals: { 11 | 'TEST_FIXTURES_DIR': Path.resolve('test_fixtures/') 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/analysers/code_maat/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk 2 | 3 | WORKDIR /codemaat 4 | COPY ./code-maat-1.0.1-standalone.jar ./codemaat-standalone.jar 5 | 6 | ENTRYPOINT ["java", "-Djava.awt.headless=true", "-jar", "codemaat-standalone.jar"] 7 | 8 | CMD ["-h"] 9 | -------------------------------------------------------------------------------- /lib/analysers/code_maat/code-maat-1.0.1-standalone.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smontanari/code-forensics/b32df000ade0dd4135622134cb4bba2f06465d8b/lib/analysers/code_maat/code-maat-1.0.1-standalone.jar -------------------------------------------------------------------------------- /lib/analysers/code_maat/docker_command_definition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2020 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var command = require('../../command'), 9 | appConfig = require('../../runtime/app_config'); 10 | 11 | var CONTAINER_DATA_FOLDER = '/data'; 12 | var dockerCommandDefinition = function() { 13 | var dockerImage = appConfig.get('codeMaat.docker.image'); 14 | var hostVolume = appConfig.get('codeMaat.docker.volume') || process.cwd(); 15 | command.Command.definitions.addDefinition('codemaat-docker', { 16 | cmd: 'docker', 17 | args: [ 18 | 'run', 19 | '--rm', 20 | '-v', 21 | hostVolume + ':' + CONTAINER_DATA_FOLDER, 22 | dockerImage 23 | ], 24 | installCheck: function() { 25 | this.verifyExecutable('docker', 'Cannot find the docker commmand.'); 26 | this.verifyConfigurationProperty(appConfig, 'codeMaat.docker.image', 'Missing required configuration property "codeMaat.docker.image"'); 27 | }, 28 | config: { 29 | hostVolume: hostVolume, 30 | containerVolume: CONTAINER_DATA_FOLDER 31 | } 32 | }); 33 | }; 34 | 35 | module.exports = dockerCommandDefinition; 36 | -------------------------------------------------------------------------------- /lib/analysers/code_maat/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var CodeMaatAnalyser = require('./code_maat_analyser'), 9 | defineJavaCommand = require('./java_command_definition'), 10 | defineDockerCommand = require('./docker_command_definition'), 11 | utils = require('../../utils'); 12 | 13 | var factory = new utils.SingletonFactory(CodeMaatAnalyser); 14 | defineJavaCommand(); 15 | defineDockerCommand(); 16 | 17 | module.exports = { 18 | analyser: function(parserInstruction) { 19 | return factory.instance(parserInstruction); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /lib/analysers/code_maat/java_command_definition.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2020 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Path = require('path'); 9 | 10 | var command = require('../../command'), 11 | appConfig = require('../../runtime/app_config'); 12 | 13 | module.exports = function() { 14 | var codeMaatPackage = appConfig.get('codeMaat.packageFile') || Path.join(__dirname, 'code-maat-1.0.1-standalone.jar'); 15 | command.Command.definitions.addDefinition('codemaat', { 16 | cmd: 'java', 17 | args: [ 18 | '-Djava.awt.headless=true', 19 | { '-jar': codeMaatPackage } 20 | ], 21 | installCheck: function() { 22 | this.verifyExecutable('java', 'Cannot find the java commmand.'); 23 | this.verifyFile(codeMaatPackage, 'Cannot find the codemaat jar at: ' + codeMaatPackage); 24 | } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/analysers/escomplex/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ESComplexAnalyser = require('./escomplex_analyser'), 9 | utils = require('../../utils'); 10 | 11 | var factory = new utils.SingletonFactory(ESComplexAnalyser); 12 | 13 | module.exports = { 14 | analyser: function() { 15 | return factory.instance(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/analysers/flog/flog_analyser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | StringDecoder = require('string_decoder').StringDecoder, 10 | duplexer2 = require('duplexer2'), 11 | logger = require('../../log'), 12 | utils = require('../../utils'), 13 | command = require('../../command'); 14 | 15 | command.Command.definitions.addDefinition('flog', { 16 | cmd: 'flog', 17 | args: ['-a', '-'], 18 | installCheck: function() { 19 | this.verifyExecutable('ruby', 'Cannot find the ruby commmand.'); 20 | this.verifyPackage('gem list flog -i', 'true', 'Missing "flog" gem'); 21 | } 22 | }); 23 | 24 | module.exports = function(parser) { 25 | command.Command.ensure('flog'); 26 | 27 | var decoder = new StringDecoder(); 28 | var analyse = function(filepath, content, transformFn) { 29 | var report = _.extend({ path: filepath }, parser.read(decoder.write(content))); 30 | if (_.isFunction(transformFn)) { 31 | return transformFn(report); 32 | } 33 | return report; 34 | }; 35 | 36 | this.fileAnalysisStream = function(filepath, transformFn) { 37 | logger.info('Analysing ', filepath); 38 | return command.stream('flog', [filepath]) 39 | .pipe(utils.stream.reduceToObjectStream(function(data) { 40 | return analyse(filepath, data, transformFn); 41 | })); 42 | }; 43 | 44 | this.sourceAnalysisStream = function(filepath, transformFn) { 45 | var proc = command.createAsync('flog', []); 46 | var outputStream = proc.stdout.pipe(utils.stream.reduceToObjectStream(function(data) { 47 | return analyse(filepath, data, transformFn); 48 | })); 49 | return duplexer2({ readableObjectMode: true }, proc.stdin, outputStream); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/analysers/flog/flog_parser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = function() { 11 | var REGEXP = /^\s+(\d+\.\d+): (.*)$/; 12 | 13 | var readLine = function(line) { 14 | var match = REGEXP.exec(line); 15 | if (match) { 16 | return { 17 | label: match[2], 18 | value: parseFloat(match[1]) 19 | }; 20 | } 21 | }; 22 | 23 | this.read = function(content) { 24 | var lines = content.split('\n'); 25 | return _.reduce(lines, function(result, line) { 26 | var metric = readLine(line); 27 | if (metric) { 28 | if (metric.label === 'flog total') { 29 | result.totalComplexity = metric.value; 30 | } else if (metric.label === 'flog/method average') { 31 | result.averageComplexity = metric.value; 32 | } else { 33 | result.methodComplexity.push({ name: metric.label, complexity: metric.value }); 34 | } 35 | } 36 | return result; 37 | }, { methodComplexity: [] }); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/analysers/flog/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var FlogAnalyser = require('./flog_analyser'), 9 | FlogParser = require('./flog_parser'), 10 | utils = require('../../utils'); 11 | 12 | var factory = new utils.SingletonFactory(FlogAnalyser); 13 | var flogParser = new FlogParser(); 14 | 15 | module.exports = { 16 | analyser: function() { 17 | return factory.instance(flogParser); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/analysers/sloc/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var SlocAnalyser = require('./sloc_analyser'), 9 | utils = require('../../utils'); 10 | 11 | var factory = new utils.SingletonFactory(SlocAnalyser); 12 | 13 | module.exports = { 14 | analyser: function() { 15 | return factory.instance(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/analysers/word_count/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var WordCountAnalyser = require('./word_count_analyser'), 9 | utils = require('../../utils'); 10 | 11 | var factory = new utils.SingletonFactory(WordCountAnalyser); 12 | 13 | module.exports = { 14 | analyser: function() { 15 | return factory.instance(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/analysers/word_count/text_filters.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports.createRejectFn = function(blacklistExpressions) { 11 | return function(text) { 12 | return _.some((blacklistExpressions || []), function(exp) { 13 | if (_.isString(exp)) { return exp === text; } 14 | else if (_.isRegExp(exp)) { return exp.test(text); } 15 | else if (_.isFunction(exp)) { return exp.call(null, text); } 16 | }); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/analysers/word_count/word_count_analyser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | filter = require('through2-filter'), 10 | StringDecoder = require('string_decoder').StringDecoder, 11 | through2 = require('through2'), 12 | multipipe = require('multipipe'), 13 | LineStream = require('byline').LineStream; 14 | 15 | var textFilters = require('./text_filters'), 16 | WordsCounter = require('./word_counter'); 17 | 18 | var decoder = new StringDecoder(); 19 | 20 | module.exports = function() { 21 | this.textAnalysisStream = function(filters) { 22 | var rejectText = textFilters.createRejectFn(filters); 23 | var counter = new WordsCounter(); 24 | 25 | return multipipe( 26 | new LineStream(), 27 | filter(function(line) { 28 | return !rejectText(decoder.write(line).toLowerCase()); 29 | }), 30 | through2.obj(function(line, enc, callback) { 31 | var words = decoder.write(line).toLowerCase().split(/\s+/); 32 | counter.addWords(_.reject(words, rejectText)); 33 | callback(); 34 | }, function(callback) { 35 | this.push(counter.report()); 36 | callback(); 37 | }) 38 | ); 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /lib/analysers/word_count/word_counter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = function() { 11 | var dictionary = []; 12 | 13 | this.addWords = function(words) { 14 | _.each(words, function(word) { 15 | var entry = _.find(dictionary, { text: word }); 16 | if (entry) { entry.count++; } 17 | else { 18 | dictionary.push({ text: word, count: 1 }); 19 | } 20 | }); 21 | }; 22 | 23 | this.report = function() { 24 | return _.sortBy(dictionary, function(w) { return -w.count; }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/api_middleware/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var router = require('koa-route'), 9 | JSONStream = require('JSONStream'), 10 | multistream = require('multistream'), 11 | loadReports = require('./load_reports'); 12 | 13 | var ApiMiddleware = new Function(); 14 | ApiMiddleware.prototype.optionDefinitions = function() { 15 | return [ 16 | { name: 'reportDir', type: String, description: 'Reports output folder' } 17 | ]; 18 | }; 19 | ApiMiddleware.prototype.middleware = function(config) { 20 | return [ 21 | router.get('/allReports', function(ctx) { 22 | ctx.response.type = 'json'; 23 | ctx.response.status = 200; 24 | 25 | ctx.response.body = multistream.obj(loadReports(config.reportDir)) 26 | .pipe(JSONStream.stringify('[\n', ',\n', '\n]\n')); 27 | }) 28 | ]; 29 | }; 30 | 31 | module.exports = ApiMiddleware; 32 | -------------------------------------------------------------------------------- /lib/api_middleware/load_reports.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | glob = require('glob'), 10 | Path = require('path'), 11 | StringDecoder = require('string_decoder').StringDecoder, 12 | logger = require('../log'), 13 | utils = require('../utils'); 14 | 15 | var decoder = new StringDecoder(); 16 | 17 | module.exports = function(baseDir) { 18 | return _.reduce(glob.sync(Path.join(baseDir, '*')), function(allStreams, reportDir) { 19 | var manifestFile = Path.join(reportDir, 'manifest.json'); 20 | if (utils.fileSystem.isFile(manifestFile)) { 21 | allStreams.push(utils.stream.readFileToObjectStream(manifestFile, function(content) { 22 | return JSON.parse(decoder.write(content)); 23 | })); 24 | } else { 25 | logger.error('Report manifest not found: ' + manifestFile); 26 | } 27 | return allStreams; 28 | }, []); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/command/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Command = require('./command.js'); 10 | 11 | var createProcess = function(type, progName, args, opts) { 12 | return new Command(progName, args, opts)[type + 'Process'](); 13 | }; 14 | 15 | module.exports = { 16 | Command: Command, 17 | createSync: _.wrap('sync', createProcess), 18 | createAsync: _.wrap('async', createProcess), 19 | run: function() { 20 | var args = ['sync'].concat(_.toArray(arguments)); 21 | return createProcess.apply(null, args).stdout; 22 | }, 23 | stream: function() { 24 | var args = ['async'].concat(_.toArray(arguments)); 25 | return createProcess.apply(null, args).stdout; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lib/graph_support/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | Tree: require('./tree'), 10 | WeightedTree: require('./weighted_tree'), 11 | WeightedCollection: require('./weighted_collection') 12 | }; 13 | -------------------------------------------------------------------------------- /lib/graph_support/tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var TreeNode = require('./tree_node'); 11 | 12 | module.exports = function(rootName, nameProperty) { 13 | this.rootNode = new TreeNode(); 14 | var rootNameRegexp = new RegExp('^' + rootName + '/'); 15 | 16 | this.addNode = function(item) { 17 | var name = item[nameProperty].replace(rootNameRegexp, ''); 18 | var childNode = this.rootNode.getChildNode(name); 19 | _.forOwn(item, function(value, key) { 20 | if (key === nameProperty) { return; } 21 | childNode[key] = value; 22 | }); 23 | return childNode; 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/graph_support/tree_node.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var TreeNode = function(nodeName) { 11 | this.name = nodeName; 12 | this.children = []; 13 | }; 14 | 15 | TreeNode.prototype.getChildNode = function(nodeName) { 16 | return _.reduce(nodeName.split('/'), function(node, name) { 17 | var child = _.find(node.children, function(childNode) { return childNode.name === name; }); 18 | if (_.isUndefined(child)) { 19 | child = new TreeNode(name); 20 | node.children.push(child); 21 | } 22 | return child; 23 | }, this); 24 | }; 25 | 26 | module.exports = TreeNode; 27 | -------------------------------------------------------------------------------- /lib/graph_support/weighted_collection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | logger = require('../log'); 10 | 11 | module.exports = function(weightedProperty, normalised) { 12 | var collection = []; 13 | var isNormalised = Boolean(normalised); 14 | 15 | var extractWeight = function(item, property) { 16 | if (_.isFunction(property)) { return property(item); } 17 | if (_.isString(property)) { return item[property]; } 18 | throw new Error('Property must be a function or a property name'); 19 | }; 20 | 21 | var weightCalculatorFn = function() { 22 | var maxWeight = _.maxBy(collection, 'weight').weight; 23 | if (maxWeight === 0) { 24 | logger.warn("Can't determine weight of collection. Assigning a value of 0 to every item."); 25 | return _.constant(0); 26 | } 27 | if (isNormalised) { 28 | return function(obj) { return obj.weight * 1.0 / maxWeight; }; 29 | } 30 | return _.property('weight'); 31 | }; 32 | 33 | this.addItem = function(item) { 34 | var weight = extractWeight(item, weightedProperty) || 0; 35 | collection.push({ item: item, weight: weight }); 36 | }; 37 | 38 | this.assignWeights = function(weightPropertyName) { 39 | if (!_.isEmpty(collection)) { 40 | var property = weightPropertyName || 'weight'; 41 | var weightCalculator = weightCalculatorFn(); 42 | _.each(collection, function(obj) { 43 | obj.item[property] = weightCalculator(obj); 44 | }); 45 | } 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/graph_support/weighted_tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var WeightedCollection = require('./weighted_collection'), 9 | Tree = require('./tree'); 10 | 11 | module.exports = function(rootName, nameProperty, weightOptions) { 12 | var tree = new Tree(rootName, nameProperty); 13 | var weightedData = new WeightedCollection(weightOptions.weightedProperty, weightOptions.normalised); 14 | 15 | this.withItem = function(item) { 16 | weightedData.addItem(tree.addNode(item)); 17 | }; 18 | 19 | this.rootNode = function() { 20 | weightedData.assignWeights(weightOptions.weightPropertyName); 21 | return tree.rootNode; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | minimist = require('minimist'), 10 | mkdirp = require('mkdirp'), 11 | glob = require('glob'), 12 | logger = require('./log'), 13 | TaskDefinitions = require('./models/task/task_definitions'), 14 | taskHelpers = require('./tasks/helpers'), 15 | TaskContext = require('./runtime/task_context'), 16 | CFValidationError = require('./runtime/errors').CFValidationError; 17 | 18 | module.exports.configure = function(configuration, params) { 19 | var parameters = _.extend({}, params, minimist(process.argv.slice(2))); 20 | try { 21 | var context = new TaskContext(configuration, parameters); 22 | var taskDefinitions = new TaskDefinitions(context); 23 | var helpers = taskHelpers(context); 24 | _.each(glob.sync('./tasks/**/*_tasks.js', { cwd: __dirname }), function(taskPath) { 25 | require(taskPath)(taskDefinitions, context, helpers).tasks(); 26 | }); 27 | 28 | mkdirp.sync(context.tempDir); 29 | mkdirp.sync(context.outputDir); 30 | } catch (e) { 31 | if (e instanceof CFValidationError) { 32 | logger.error(e.message); 33 | if (e.showStack) { 34 | logger.log(e.stack); 35 | } 36 | process.exit(1); 37 | } else { 38 | throw e; 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /lib/log/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ansi = require('ansi-colors'), 9 | moment = require('moment'), 10 | appConfig = require('../runtime/app_config'); 11 | 12 | var log = function() { 13 | if (appConfig.get('logEnabled')) { 14 | var time = moment().format('[[]HH:mm:ss[]] '); 15 | process.stdout.write(time); 16 | console.log.apply(console, arguments); //eslint-disable-line no-console 17 | } 18 | }; 19 | 20 | module.exports = { 21 | log: log, 22 | debug: function(msg, detail) { log(ansi.green(msg) + (detail || '')); }, 23 | info: function(msg, detail) { log(ansi.yellow(msg) + (detail || '')); }, 24 | warn: function(msg) { log(ansi.magenta(msg)); }, 25 | error: function(msg) { log(ansi.red(msg)); } 26 | // warn: function(msg) { log(ansi.bgWhite.magenta(msg)); }, 27 | // error: function(msg) { log(ansi.bgWhite.red(msg)); } 28 | }; 29 | -------------------------------------------------------------------------------- /lib/models/developers_info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | CFValidationError = require('../runtime/errors').CFValidationError; 10 | 11 | var verifyNoDuplicateNames = function(names) { 12 | _.each(names, function(name, index, coll) { 13 | if (_.findIndex(coll, function(n) { return n === name; }, index + 1) !== -1) { 14 | throw new CFValidationError('Duplicate developer name: ' + name); 15 | } 16 | }); 17 | }; 18 | 19 | var Team = function(name, memberList) { 20 | var members = (_.isUndefined(memberList) || _.isEmpty(memberList)) ? [] : memberList; 21 | this.name = name; 22 | 23 | this.findDeveloperName = function(name) { 24 | var index = _.findIndex(members, function(devName) { 25 | return (_.isArray(devName) && _.includes(devName, name)) || devName === name; 26 | }); 27 | if (index !== -1) { 28 | var devName = members[index]; 29 | return _.isArray(devName) ? _.first(devName) : devName; 30 | } 31 | }; 32 | }; 33 | 34 | module.exports = function(devInfoData) { 35 | var teams, defaultTeam; 36 | 37 | verifyNoDuplicateNames(_.flatMapDeep(_.values(devInfoData))); 38 | 39 | if (_.isPlainObject(devInfoData) && !_.isEmpty(devInfoData)) { 40 | teams = _.map(devInfoData, function(members, name) { 41 | return new Team(name, members); 42 | }); 43 | } else { 44 | defaultTeam = new Team(undefined, devInfoData); 45 | } 46 | 47 | var findDeveloperInTeams = function(name) { 48 | var devInfo; 49 | _.each(teams, function(team) { 50 | var devName = team.findDeveloperName(name); 51 | if (devName) { 52 | devInfo = { name: devName, team: team.name }; 53 | return false; 54 | } 55 | }); 56 | return devInfo || { name: name, team: 'N/A (' + name + ')' }; 57 | }; 58 | 59 | this.hasTeamInfo = _.isArray(teams); 60 | 61 | this.find = function(name) { 62 | if (teams) { 63 | return findDeveloperInTeams(name); 64 | } 65 | return { name: defaultTeam.findDeveloperName(name) || name }; 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /lib/models/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | DevelopersInfo: require('./developers_info'), 10 | Repository: require('./repository'), 11 | TimePeriod: require('./time_interval/time_period'), 12 | TimeIntervalBuilder: require('./time_interval/builder'), 13 | LanguageDefinitions: require('./language_definitions'), 14 | LayerGrouping: require('./layer_grouping') 15 | }; 16 | -------------------------------------------------------------------------------- /lib/models/language_definitions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var DefinitionsArchive = require('../utils').DefinitionsArchive; 11 | 12 | module.exports = _.tap(new DefinitionsArchive(), function(archive) { 13 | _.each({ 14 | ruby: ['rb'], 15 | javascript: ['js'] 16 | }, function(extensions, lang) { archive.addDefinition(lang, extensions); }); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/models/layer_grouping.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var Layer = function(layerConfig) { 11 | this.name = _.kebabCase(layerConfig.name); 12 | this.value = layerConfig.name; 13 | 14 | this.toString = function() { 15 | return _.reduce(layerConfig.paths, function(lines, path) { 16 | var pathString = path.toString(); 17 | if (_.isRegExp(path)) { 18 | pathString = path.toString().slice(1, -1); 19 | if (!_.startsWith(pathString, '^')) { pathString = '^' + pathString; } 20 | if (!_.endsWith(pathString, '$')) { pathString += '$'; } 21 | } 22 | lines.push(pathString + ' => ' + layerConfig.name); 23 | return lines; 24 | }, []).join('\n'); 25 | }; 26 | }; 27 | 28 | module.exports = function(config) { 29 | var layers = _.map(config, function(layerConfig) { 30 | return new Layer(layerConfig); 31 | }); 32 | 33 | this.isEmpty = function() { 34 | return _.isEmpty(layers); 35 | }; 36 | 37 | this.each = _.wrap(layers, _.each); 38 | this.map = _.wrap(layers, _.map); 39 | 40 | this.toString = function() { 41 | return _.reduce(layers, function(lines, layer) { 42 | lines.push(layer.toString()); 43 | return lines; 44 | }, []).join('\n'); 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/models/repository_path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Path = require('path'), 10 | glob = require('glob'), 11 | utils = require('../utils'); 12 | 13 | module.exports = { 14 | makeGlob: function(path) { 15 | if (!glob.hasMagic(path) && utils.fileSystem.isDirectory(path)) { 16 | return Path.join(path, '**/*'); 17 | } else { 18 | return path; 19 | } 20 | }, 21 | expand: function(originalPaths, expander) { 22 | return _.reduce(originalPaths, function(array, path) { 23 | var expandedPaths = _.filter(expander(path), function(p) { 24 | return utils.fileSystem.isFile(p); 25 | }); 26 | return array.concat(expandedPaths); 27 | }, []); 28 | }, 29 | normalise: function(absoluteRootPath, paths) { 30 | var self = this; 31 | return _.map( 32 | _.map(paths, function(p) { return Path.join(absoluteRootPath, p); }), 33 | self.makeGlob 34 | ); 35 | }, 36 | relativise: function(absoluteRootPath, path) { 37 | var rootPath = absoluteRootPath.endsWith('/') ? absoluteRootPath : absoluteRootPath.concat('/'); 38 | if (path.startsWith(rootPath)) { 39 | return path.substring(rootPath.length); 40 | } 41 | return path; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /lib/models/task/gulp_task.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | CFValidationError = require('../../runtime/errors').CFValidationError; 10 | 11 | var DEFAULT_TASK_INFO = { 12 | description: 'No description available', 13 | parameters: [] 14 | }; 15 | 16 | var usageInfo = function(parameters) { 17 | var paramsInfo = function(params) { 18 | return _.map(params, function(param) { 19 | return '--' + param.name + (param.isFlag ? '' : '=<' + param.name + '>'); 20 | }); 21 | }; 22 | 23 | var allParams = _.partition(parameters, 'required'); 24 | var requiredParams = paramsInfo(allParams[0]); 25 | var optionalParams = _.map(paramsInfo(allParams[1]), function(p) { return '[' + p + ']'; }); 26 | 27 | return _.compact(_.concat(requiredParams, optionalParams)).join(' '); 28 | }; 29 | 30 | module.exports = function() { 31 | var parseArguments = function(args) { 32 | this.name = args.shift(); 33 | var taskInfo = _.isPlainObject(_.first(args)) ? _.defaults(args.shift(), DEFAULT_TASK_INFO) : DEFAULT_TASK_INFO; 34 | _.assign(this, taskInfo); 35 | this.dependency = args.shift(); 36 | }; 37 | 38 | parseArguments.call(this, _.toArray(arguments)); 39 | 40 | this.usage = _.compact(['gulp', this.name, usageInfo(this.parameters)]).join(' '); 41 | 42 | this.validateParameters = function(params) { 43 | _.each(_.filter(this.parameters, 'required'), function(parameter) { 44 | if (_.isNil(params[parameter.name])) { 45 | throw new CFValidationError('Required parameter missing: ' + parameter.name); 46 | } 47 | }); 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/models/task/runners/default_runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | 11 | module.exports = function(task) { 12 | var doNothing = function(done) { done(); }; 13 | 14 | if (_.isFunction(task.run)) { 15 | this.run = task.run; 16 | } else { 17 | this.run = doNothing; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/models/task/runners/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | Default: require('./default_runner'), 10 | Report: require('./report_runner') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/models/task/runners/report_runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | isStream = require('is-stream'), 10 | Bluebird = require('bluebird'), 11 | reporting = require('../../../reporting'), 12 | logger = require('../../../log'), 13 | utils = require('../../../utils'); 14 | 15 | 16 | module.exports = function(task, context) { 17 | var doNothing = function(done) { done(); }; 18 | var publishReport = function(done) { 19 | var publisher = new reporting.Publisher(task, context); 20 | 21 | try { 22 | var returnedValue = task.run.call(null, publisher); 23 | var promise = isStream(returnedValue) ? utils.stream.streamToPromise(returnedValue) : Bluebird.resolve(returnedValue); 24 | 25 | return promise 26 | .then(publisher.createManifest.bind(publisher)) 27 | .catch(logger.error); 28 | } catch (e) { 29 | logger.error(e); 30 | done(); 31 | } 32 | }; 33 | 34 | if (_.isFunction(task.run)) { 35 | this.run = publishReport; 36 | } else { 37 | this.run = doNothing; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /lib/models/time_interval/builder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var moment = require('moment'), 9 | _ = require('lodash'), 10 | CFValidationError = require('../../runtime/errors').CFValidationError, 11 | TimePeriod = require('./time_period'), 12 | TimeSplitter = require('./time_splitter'); 13 | 14 | module.exports = function(dateFormat) { 15 | var timeSplit, 16 | dateFrom = moment(), 17 | dateTo = moment(); 18 | 19 | var parseDate = function(d) { 20 | var date = moment(d, dateFormat); 21 | if (!date.isValid()) { 22 | throw new CFValidationError('Error parsing date: ' + d); 23 | } 24 | return date; 25 | }; 26 | 27 | this.from = function(from) { 28 | if (_.isString(from)) { 29 | dateFrom = parseDate(from); 30 | } 31 | return this; 32 | }; 33 | 34 | this.to = function(to) { 35 | if (_.isString(to)) { 36 | dateTo = parseDate(to); 37 | } 38 | return this; 39 | }; 40 | 41 | this.split = function(split) { 42 | timeSplit = split; 43 | return this; 44 | }; 45 | 46 | this.build = function() { 47 | if (dateTo.isBefore(dateFrom)) { 48 | throw new CFValidationError('The to Date (' + dateTo.toString() + ') cannot be before than the from Date (' + dateFrom.toString() + ')'); 49 | } 50 | 51 | return _.map(new TimeSplitter(dateFrom, dateTo).split(timeSplit), function(period) { 52 | return new TimePeriod(period, dateFormat); 53 | }); 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/models/time_interval/time_period.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var TimePeriod = function(period, dateFormat) { 9 | this.startDate = period.start; 10 | this.endDate = period.end; 11 | this.dateFormat = dateFormat; 12 | }; 13 | 14 | TimePeriod.prototype.toString = function() { 15 | return this.startDate.format(this.dateFormat) + '_' + this.endDate.format(this.dateFormat); 16 | }; 17 | 18 | TimePeriod.prototype.toDisplayFormat = function() { 19 | return { 20 | startDate: this.startDate.format(this.dateFormat), 21 | endDate: this.endDate.format(this.dateFormat) 22 | }; 23 | }; 24 | 25 | TimePeriod.prototype.toISOFormat = function() { 26 | return { 27 | startDate: this.startDate.toISOString(), 28 | endDate: this.endDate.toISOString() 29 | }; 30 | }; 31 | 32 | module.exports = TimePeriod; 33 | -------------------------------------------------------------------------------- /lib/parallel_processing/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var os = require('os'), 9 | TaskExecutor = require('./task_executor'), 10 | StreamProcessor = require('./stream_processor'), 11 | JobScheduler = require('./job_scheduler'), 12 | appConfig = require('../runtime/app_config'); 13 | 14 | var concurrency = appConfig.get('serialProcessing') ? 1 : os.cpus().length; 15 | var taskJobScheduler = new JobScheduler(concurrency); 16 | var streamJobScheduler = new JobScheduler(concurrency); 17 | 18 | module.exports = { 19 | taskExecutor: function() { 20 | return new TaskExecutor(taskJobScheduler); 21 | }, 22 | streamProcessor: function() { 23 | return new StreamProcessor(streamJobScheduler); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /lib/parallel_processing/job_scheduler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Bluebird = require('bluebird'), 9 | logger = require('../log'); 10 | 11 | module.exports = function(maxParallelProcs) { 12 | var maxConcurrentJobs = maxParallelProcs || 1; 13 | var activeProcs = 0; 14 | 15 | var jobQueue = []; 16 | 17 | var onJobFinished = function() { 18 | activeProcs--; 19 | executeNext(); 20 | }; 21 | 22 | var executeNext = function() { 23 | if (jobQueue.length > 0 && activeProcs < maxConcurrentJobs) { 24 | activeProcs++; 25 | jobQueue.shift().call().then(onJobFinished); 26 | } 27 | }; 28 | 29 | this.addJob = function(jobFn) { 30 | return new Bluebird(function(resolve, reject) { 31 | jobQueue.push(function() { 32 | return Bluebird.try(jobFn) 33 | .then(resolve) 34 | .catch(function(err) { 35 | logger.warn(err); 36 | reject(err); 37 | }); 38 | }); 39 | executeNext(); 40 | }); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /lib/parallel_processing/stream_processor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Bluebird = require('bluebird'), 10 | merge2 = require('merge2'), 11 | logger = require('../log'), 12 | utils = require('../utils'); 13 | 14 | module.exports = function(jobScheduler) { 15 | this.mergeAll = function(iterable, streamFn) { 16 | var mergedStream = merge2({ end: false }); 17 | var streamPromises = _.map(iterable, function(item) { 18 | return jobScheduler.addJob(function() { 19 | var stream = streamFn(item); 20 | mergedStream.add(stream); 21 | return utils.stream.streamToPromise(stream) 22 | .catch(function(err) { 23 | logger.warn(err); 24 | }); 25 | }); 26 | }); 27 | 28 | Bluebird.all(streamPromises).then(function() { 29 | mergedStream.end(); 30 | }); 31 | return mergedStream; 32 | }; 33 | 34 | this.process = function(streamFn) { 35 | return jobScheduler.addJob(function() { 36 | return utils.stream.streamToPromise(streamFn()); 37 | }).reflect(); 38 | }; 39 | 40 | this.processAll = function(iterable, streamFn) { 41 | var self = this; 42 | var streamPromises = _.map(iterable, function(item) { 43 | return self.process(_.wrap(item, streamFn)); 44 | }); 45 | return Bluebird.all(streamPromises); 46 | }; 47 | 48 | this.read = function(streamFn) { 49 | return jobScheduler.addJob(function() { 50 | return utils.stream.objectStreamToArray(streamFn()); 51 | }); 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /lib/parallel_processing/task_executor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Bluebird = require('bluebird'); 10 | 11 | module.exports = function(jobScheduler) { 12 | var processTask = function(task) { 13 | if (_.isFunction(task)) { 14 | return Bluebird.try(task); 15 | } 16 | return Bluebird.resolve(task); 17 | }; 18 | 19 | this.runAll = function(iterable, taskFn) { 20 | var tasksPromises = _.map(iterable, function(item) { 21 | return jobScheduler.addJob(function() { 22 | var taskOutput = _.isFunction(taskFn) ? taskFn(item) : item(); 23 | return processTask(taskOutput); 24 | }).reflect(); 25 | }); 26 | return Bluebird.all(tasksPromises); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /lib/reporting/data_source_handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | isStream = require('is-stream'), 10 | Bluebird = require('bluebird'), 11 | utils = require('../utils'); 12 | 13 | var handleSource = function(source) { 14 | if (_.isArray(source)) { 15 | return Bluebird.resolve(source); 16 | } else if (isStream.readable(source)) { 17 | return utils.stream.objectStreamToArray(source); 18 | } else if (utils.fileSystem.isFile(source)) { 19 | return utils.json.fileToObject(source); 20 | } else { 21 | return Bluebird.reject(new Error('Invalid report source data: ' + source)); 22 | } 23 | }; 24 | 25 | var Handler = function() { 26 | this.processDataSource = function(source) { 27 | if (_.isFunction(source)) { 28 | return handleSource(source()); 29 | } else { 30 | return Bluebird.resolve(source).then(handleSource); 31 | } 32 | }; 33 | }; 34 | 35 | module.exports = { 36 | instance: function() { return new Handler(); } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/reporting/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | ReportComposer: require('./report_composer'), 10 | MergeStrategies: require('./merge_strategies'), 11 | Publisher: require('./publisher') 12 | }; 13 | -------------------------------------------------------------------------------- /lib/reporting/merge_strategies.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | objectTransformer = require('./object_transformer'); 10 | 11 | module.exports = { 12 | extension: function(transformOption) { 13 | return function(reportItem, dataSourceItem) { 14 | _.extend(reportItem, objectTransformer(dataSourceItem, transformOption)); 15 | }; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/reporting/object_transformer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(obj, option) { 11 | if (_.isFunction(option)) { return option(obj); } 12 | if (_.isString(option) || _.isArray(option)) { return _.pick(obj, option); } 13 | return obj; 14 | }; 15 | -------------------------------------------------------------------------------- /lib/reporting/report_composer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Bluebird = require('bluebird'), 10 | DataSourceHandler = require('./data_source_handler'); 11 | 12 | var ReportComposer = function(initialDataSource) { 13 | var dataSourceHandler = DataSourceHandler.instance(); 14 | var dataSources = []; 15 | 16 | var mergeAllReports = function(reportData) { 17 | return Bluebird.map(dataSources, function(ds) { 18 | return dataSourceHandler.processDataSource(ds.source).then(function(data) { 19 | _.each(reportData, function(reportItem) { 20 | _.each(_.filter(data, _.partial(ds.mergeOptions.matchStrategy, reportItem)), function(dataItem) { 21 | return ds.mergeOptions.mergeStrategy(reportItem, dataItem); 22 | }); 23 | }); 24 | }); 25 | }); 26 | }; 27 | 28 | this.mergeWith = function() { 29 | dataSources.push(ReportComposer.newDataSource.apply(null, arguments)); 30 | return this; 31 | }; 32 | 33 | this.mergeAll = function(sources) { 34 | dataSources = dataSources.concat(sources); 35 | return this; 36 | }; 37 | 38 | this.buildReport = function() { 39 | return dataSourceHandler.processDataSource(initialDataSource).then(function(reportData) { 40 | return mergeAllReports(reportData).then(function() { 41 | return reportData; 42 | }); 43 | }); 44 | }; 45 | }; 46 | 47 | ReportComposer.newDataSource = function(source, mergeOptions) { 48 | return { source: source, mergeOptions: mergeOptions }; 49 | }; 50 | 51 | module.exports = ReportComposer; 52 | -------------------------------------------------------------------------------- /lib/runtime/app_config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Path = require('path'), 10 | FileConfigReader = require('./file_config_reader'), 11 | EnvConfigReader = require('./env_config_reader'); 12 | 13 | var DEFAULT_CONFIG = { 14 | basedir: Path.resolve(Path.join(__dirname, '..', '..')), 15 | versionControlSystem: 'git', 16 | serialProcessing: false, 17 | debugMode: false, 18 | logEnabled: true, 19 | serverPort: 3000 20 | }; 21 | 22 | var fileConfigReader = new FileConfigReader(); 23 | var envConfigReader = new EnvConfigReader(); 24 | 25 | var configurationInstance = {}; 26 | var getConfiguration = function() { 27 | if (_.isEmpty(configurationInstance)) { 28 | configurationInstance = _.defaultsDeep(envConfigReader.getConfiguration(), fileConfigReader.getConfiguration(), DEFAULT_CONFIG); 29 | } 30 | return configurationInstance; 31 | }; 32 | 33 | module.exports = { 34 | get: function(property) { 35 | return _.get(getConfiguration(), property); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/runtime/defaults.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2020 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | parameters: { 10 | maxCoupledFiles: 5, 11 | minWordCount: 5 12 | }, 13 | configuration: { 14 | tempDir: 'tmp', 15 | outputDir: 'output', 16 | dateFormat: 'YYYY-MM-DD', 17 | languages: [] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/runtime/env_config_reader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | utils = require('../utils'); 10 | 11 | module.exports = function() { 12 | var serverPort = parseInt(process.env.SERVER_PORT); 13 | var codeMaatOptions = process.env.CODEMAAT_OPTS; 14 | 15 | this.getConfiguration = function() { 16 | return { 17 | serialProcessing: !_.isUndefined(process.env.SERIAL_PROCESSING), 18 | debugMode: !_.isUndefined(process.env.COMMAND_DEBUG), 19 | logEnabled: _.isUndefined(process.env.LOG_DISABLED), 20 | serverPort: _.isInteger(serverPort) ? serverPort : undefined, 21 | codeMaat: { options: _.isString(codeMaatOptions) ? utils.arrays.arrayPairsToObject(codeMaatOptions.split(' ')) : {} } 22 | }; 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /lib/runtime/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var appConfig = require('./app_config'); 9 | 10 | var errorFunction = function(errorName) { 11 | var ErrorFunction = function(msg) { 12 | var e = Error.call(this, msg); 13 | this.message = e.message; 14 | this.stack = e.stack; 15 | this.showStack = appConfig.get('debugMode'); 16 | }; 17 | ErrorFunction.prototype = Object.create(Error.prototype); 18 | ErrorFunction.prototype.name = errorName; 19 | 20 | return ErrorFunction; 21 | }; 22 | 23 | module.exports = { 24 | CFValidationError: errorFunction('CFValidationError'), 25 | CFRuntimeError: errorFunction('CFRuntimeError') 26 | }; 27 | -------------------------------------------------------------------------------- /lib/runtime/file_config_reader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Path = require('path'), 9 | fs = require('fs'), 10 | StringDecoder = require('string_decoder').StringDecoder, 11 | utils = require('../utils'); 12 | 13 | module.exports = function() { 14 | var file = Path.resolve('.code-forensics'); 15 | var decoder = new StringDecoder(); 16 | 17 | this.getConfiguration = function() { 18 | if (utils.fileSystem.isFile(file)) { 19 | return JSON.parse(decoder.write(fs.readFileSync(file))); 20 | } 21 | return {}; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/runtime/task_context.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Path = require('path'), 9 | _ = require('lodash'), 10 | logger = require('../log'), 11 | models = require('../models'); 12 | 13 | var runtimeDefaults = require('./defaults'); 14 | 15 | module.exports = function(config, params) { 16 | var configuration = _.defaults({}, config, runtimeDefaults.configuration); 17 | 18 | this.parameters = _.defaults({}, params, runtimeDefaults.parameters); 19 | this.tempDir = Path.resolve(configuration.tempDir); 20 | this.outputDir = Path.resolve(configuration.outputDir); 21 | 22 | this.repository = new models.Repository(configuration.repository); 23 | 24 | this.timePeriods = new models.TimeIntervalBuilder(configuration.dateFormat) 25 | .from(this.parameters.dateFrom) 26 | .to(this.parameters.dateTo) 27 | .split(this.parameters.timeSplit) 28 | .build(); 29 | 30 | this.dateRange = new models.TimePeriod( 31 | { start: _.first(this.timePeriods).startDate, end: _.last(this.timePeriods).endDate }, 32 | configuration.dateFormat 33 | ); 34 | 35 | this.layerGrouping = new models.LayerGrouping((configuration.layerGroups || {})[this.parameters.layerGroup]); 36 | this.developersInfo = new models.DevelopersInfo(configuration.contributors); 37 | 38 | this.languages = _.filter(configuration.languages, function(lang) { 39 | if (_.isUndefined(models.LanguageDefinitions.getDefinition(lang))) { 40 | logger.warn('Language "' + lang + '" is not supported'); 41 | return false; 42 | } 43 | return true; 44 | }); 45 | 46 | this.commitMessageFilters = configuration.commitMessageFilters; 47 | }; 48 | -------------------------------------------------------------------------------- /lib/tasks/complexity_analysis/javascript_tasks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | escomplex = require('../../analysers/escomplex'), 10 | pp = require('../../parallel_processing'), 11 | utils = require('../../utils'); 12 | 13 | module.exports = function(taskDef, context, helpers) { 14 | var javascriptComplexityReport = function() { 15 | var stream = pp.streamProcessor().mergeAll(context.repository.sourceFiles('javascript'), function(file) { 16 | return escomplex.analyser().fileAnalysisStream(file.absolutePath, function(report) { 17 | return _.extend(report, { path: file.relativePath }); 18 | }); 19 | }); 20 | 21 | return utils.json.objectArrayToFileStream(helpers.files.codeComplexity('javascript'), stream); 22 | }; 23 | 24 | return { 25 | functions: { 26 | javascriptComplexityReport: javascriptComplexityReport 27 | }, 28 | tasks: function() { 29 | taskDef.addTask('javascript-complexity-report', javascriptComplexityReport); 30 | taskDef.addAnalysisTask('javascript-complexity-trend-analysis', 31 | { 32 | description: 'Analyse the complexity trend in time for a particular javascript file', 33 | reportName: 'complexity-trend', 34 | parameters: [{ name: 'targetFile', required: true }, { name: 'dateFrom' }, { name: 'dateTo' }], 35 | reportFile: 'complexity-trend-data.json', 36 | run: function(publisher) { 37 | _.each(['total', 'func-mean', 'func-sd'], function(name) { publisher.enableDiagram(name); }); 38 | var stream = helpers.revision.revisionAnalysisStream(escomplex.analyser()); 39 | return utils.json.objectArrayToFileStream(publisher.addReportFile(), stream); 40 | } 41 | } 42 | ); 43 | } 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /lib/tasks/complexity_analysis/ruby_tasks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | pp = require('../../parallel_processing'), 10 | flog = require('../../analysers/flog'), 11 | utils = require('../../utils'); 12 | 13 | module.exports = function(taskDef, context, helpers) { 14 | var rubyComplexityReport = function() { 15 | var stream = pp.streamProcessor().mergeAll(context.repository.sourceFiles('ruby'), function(file) { 16 | return flog.analyser().fileAnalysisStream(file.absolutePath, function(report) { 17 | return _.extend(report, { path: file.relativePath }); 18 | }); 19 | }); 20 | 21 | return utils.json.objectArrayToFileStream(helpers.files.codeComplexity('ruby'), stream); 22 | }; 23 | return { 24 | functions: { 25 | rubyComplexityReport: rubyComplexityReport 26 | }, 27 | tasks: function() { 28 | taskDef.addTask('ruby-complexity-report', rubyComplexityReport); 29 | 30 | taskDef.addAnalysisTask('ruby-complexity-trend-analysis', 31 | { 32 | description: 'Analyse the complexity trend in time for a particular ruby file', 33 | reportName: 'complexity-trend', 34 | parameters: [{ name: 'targetFile', required: true }, { name: 'dateFrom' }, { name: 'dateTo' }], 35 | reportFile: 'complexity-trend-data.json', 36 | run: function(publisher) { 37 | _.each(['total', 'func-mean', 'func-sd'], function(name) { publisher.enableDiagram(name); }); 38 | var stream = helpers.revision.revisionAnalysisStream(flog.analyser()); 39 | return utils.json.objectArrayToFileStream(publisher.addReportFile(), stream); 40 | } 41 | } 42 | ); 43 | } 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /lib/tasks/helpers/code_maat_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | codeMaat = require('../../analysers/code_maat'); 10 | 11 | module.exports = function() { 12 | var self = this; 13 | 14 | _.each({ 15 | revisionsAnalysis: 'revisions', 16 | summaryAnalysis: 'summary', 17 | sumCouplingAnalysis: 'soc', 18 | temporalCouplingAnalysis: 'coupling', 19 | authorsAnalysis: 'authors', 20 | mainDevAnalysis: 'main-dev', 21 | effortAnalysis: 'entity-effort', 22 | codeOwnershipAnalysis: 'entity-ownership', 23 | communicationAnalysis: 'communication', 24 | absoluteChurnAnalysis: 'abs-churn', 25 | entityChurnAnalysis: 'entity-churn' 26 | }, function(parserInstruction, analysisFn) { 27 | self[analysisFn] = function(inputLogFile, inputGroupFile, options) { 28 | return codeMaat.analyser(parserInstruction).fileAnalysisStream(inputLogFile, inputGroupFile, options); 29 | }; 30 | self[analysisFn].isSupported = function() { return codeMaat.analyser(parserInstruction).isSupported(); }; 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/tasks/helpers/files_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Path = require('path'); 9 | 10 | module.exports = function(context) { 11 | var tempPath = function(path) { 12 | return Path.join(context.tempDir, path); 13 | }; 14 | 15 | return { 16 | vcslog: function(period) { return tempPath('vcslog_' + period.toString() + '.log'); }, 17 | vcsNormalisedLog: function(period) { return tempPath('vcslog_normalised_' + period.toString() + '.log'); }, 18 | vcsCommitMessages: function(period) { return tempPath('vcs_commit_messages_' + period.toString() + '.log'); }, 19 | sloc: function() { return tempPath('sloc-report.json'); }, 20 | layerGrouping: function(layerName) { 21 | return layerName ? tempPath('layer-group-' + layerName + '.txt') : tempPath('layer-groups.txt'); 22 | }, 23 | codeComplexity: function(language) { return tempPath(language + '-complexity-report.json'); }, 24 | revisions: function() { return tempPath('revisions-report.json'); }, 25 | authors: function() { return tempPath('authors-report.json'); }, 26 | mainDeveloper: function() { return tempPath('main-dev-report.json'); }, 27 | codeOwnership: function() { return tempPath('code-ownership-report.json'); }, 28 | effort: function() { return tempPath('effort-report.json'); } 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /lib/tasks/helpers/graph_data_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | graphSupport = require ('../../graph_support'); 10 | 11 | module.exports = function() { 12 | this.weightedTree = function(reportData, treePathPropertyName, weightedPropertyName) { 13 | return _.tap(new graphSupport.WeightedTree(null, treePathPropertyName, { weightedProperty: weightedPropertyName, normalised: true }), function(tree) { 14 | _.each(reportData, tree.withItem.bind(tree)); 15 | }).rootNode(); 16 | }; 17 | 18 | this.flatWeightedTree = function(reportData, weightedPropertyName) { 19 | var weightedList = new graphSupport.WeightedCollection(weightedPropertyName, true); 20 | _.each(reportData, weightedList.addItem); 21 | weightedList.assignWeights(); 22 | 23 | return { children: reportData }; 24 | }; 25 | 26 | this.tree = function(reportData, treePathPropertyName, nodeChildrenPropertyName) { 27 | var tree = new graphSupport.Tree(null, treePathPropertyName); 28 | _.each(reportData, function(item) { 29 | if (_.isUndefined(nodeChildrenPropertyName)) { 30 | tree.addNode(item); 31 | } else { 32 | tree.addNode(_.assign( 33 | _.omit(item, nodeChildrenPropertyName), 34 | { children: item[nodeChildrenPropertyName] } 35 | )); 36 | } 37 | }); 38 | 39 | return tree.rootNode; 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /lib/tasks/helpers/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var GraphDataHelper = require('./graph_data_helper'), 9 | RevisionHelper = require('./revision_helper'), 10 | FilesHelper = require('./files_helper'), 11 | CodeMaatHelper = require('./code_maat_helper'), 12 | DeveloperDataHelper = require('./developer_data_helper'); 13 | 14 | module.exports = function(context) { 15 | return { 16 | graphData: new GraphDataHelper(context), 17 | revision: new RevisionHelper(context), 18 | developerData: new DeveloperDataHelper(context), 19 | files: new FilesHelper(context), 20 | codeMaat: new CodeMaatHelper(context) 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/tasks/helpers/revision_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var moment = require('moment'), 9 | _ = require('lodash'), 10 | pp = require('../../parallel_processing'), 11 | map = require('through2-map'), 12 | vcs = require('../../vcs'); 13 | 14 | module.exports = function(context) { 15 | var vcsClient = vcs.client(context.repository); 16 | 17 | this.revisionAnalysisStream = function(analyser) { 18 | var file = context.parameters.targetFile; 19 | var moduleRevisions = vcsClient.revisions(file, context.dateRange); 20 | 21 | if (moduleRevisions.length === 0) { throw new Error('No revisions data found'); } 22 | 23 | return pp.streamProcessor().mergeAll(moduleRevisions, function(revisionObj) { 24 | return vcsClient.showRevisionStream(revisionObj.revisionId, file) 25 | .pipe(analyser.sourceAnalysisStream(file)) 26 | .pipe(map.obj(function(analysisResult) { 27 | return _.extend({ revision: revisionObj.revisionId, date: moment(revisionObj.date) }, analysisResult); 28 | })); 29 | }); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/churn_metrics.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var ZERO_VALUE = { addedLines: 0, deletedLines: 0, totalLines: 0 }; 11 | 12 | module.exports = { 13 | selector: function(obj) { 14 | if (isNaN(obj.addedLines) || isNaN(obj.deletedLines)) { 15 | return ZERO_VALUE; 16 | } 17 | return { 18 | addedLines: obj.addedLines, 19 | deletedLines: obj.deletedLines, 20 | totalLines: obj.addedLines - obj.deletedLines 21 | }; 22 | }, 23 | defaultValue: ZERO_VALUE, 24 | accumulatorsMap: { cumulativeLines: _.property('totalLines') } 25 | }; 26 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/coupling_metrics.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | selector: function(obj) { 10 | return { 11 | coupledName: obj.coupledPath, 12 | couplingDegree: obj.couplingDegree 13 | }; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/data_collector.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var sortStream = require('sort-stream2'), 9 | filter = require('through2-filter'), 10 | moment = require('moment'), 11 | Bluebird = require('bluebird'); 12 | 13 | module.exports = function(timePeriods) { 14 | this.collectDataStream = function(collectionStrategy) { 15 | return new Bluebird(function(resolve, reject) { 16 | if (collectionStrategy.isSupported()) { 17 | resolve(collectionStrategy.collect(timePeriods) 18 | .pipe(filter.obj(function(obj) { return obj.date && moment(obj.date, moment.ISO_8601).isValid(); })) 19 | .pipe(sortStream(function(a, b) { return moment(a.date).diff(moment(b.date)); })) 20 | .pipe(collectionStrategy.accumulator)); 21 | } 22 | reject('Data analysis not supported'); 23 | }); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/metric_collection_strategies/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | pp = require('../../../parallel_processing'), 10 | noLayerStrategy = require('./no_layer_strategy'), 11 | splitLayerStrategy = require('./split_layer_strategy'), 12 | multiLayerStrategy = require('./multi_layer_strategy'), 13 | TimePeriodResults = require('./time_period_results'); 14 | 15 | var strategyObject = function(helpers, options) { 16 | return { 17 | isSupported: function() { return helpers.codeMaat[options.analysis].isSupported(); }, 18 | accumulator: TimePeriodResults.resultsAccumulator(options.metrics.accumulatorsMap) 19 | }; 20 | }; 21 | 22 | var createStrategy = function(collectStrategyFn) { 23 | return function(helpers, options) { 24 | return _.tap(strategyObject(helpers, options), function(strategy) { 25 | var streamProcessor = pp.streamProcessor(); 26 | strategy.collect = collectStrategyFn(streamProcessor, helpers, options); 27 | }); 28 | }; 29 | }; 30 | 31 | module.exports = { 32 | noLayer: createStrategy(noLayerStrategy), 33 | multiLayer: createStrategy(multiLayerStrategy), 34 | splitLayer: createStrategy(splitLayerStrategy) 35 | }; 36 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/metric_collection_strategies/multi_layer_strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var TimePeriodResults = require('./time_period_results'); 9 | 10 | module.exports = function(streamProcessor, helpers, options) { 11 | var transformFn = TimePeriodResults.resultsMapper(options.metrics.selector); 12 | 13 | return function(timePeriods) { 14 | return streamProcessor.mergeAll(timePeriods, function(period) { 15 | var vcsLogFile = helpers.files.vcsNormalisedLog(period); 16 | return helpers.codeMaat[options.analysis](vcsLogFile, helpers.files.layerGrouping()).pipe( 17 | transformFn(period) 18 | ); 19 | }); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/metric_collection_strategies/no_layer_strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var TimePeriodResults = require('./time_period_results'); 9 | 10 | module.exports = function(streamProcessor, helpers, options) { 11 | var transformFn = TimePeriodResults.resultsReducer(options.metrics.selector, options.metrics.defaultValue); 12 | 13 | return function(timePeriods) { 14 | return streamProcessor.mergeAll(timePeriods, function(period) { 15 | var vcsLogFile = helpers.files.vcsNormalisedLog(period); 16 | return helpers.codeMaat[options.analysis](vcsLogFile) 17 | .pipe(transformFn(period)); 18 | }); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/metric_collection_strategies/split_layer_strategy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | map = require('through2-map'), 10 | TimePeriodResults = require('./time_period_results'); 11 | 12 | module.exports = function(streamProcessor, helpers, options) { 13 | var transformFn = TimePeriodResults.resultsMerger(options.metrics.selector); 14 | 15 | return function(timePeriods) { 16 | var iterator = _.flatMap(timePeriods, function(period) { 17 | return options.layerGrouping.map(function(layer) { 18 | return { timePeriod: period, layer: layer }; 19 | }); 20 | }); 21 | 22 | return streamProcessor.mergeAll(iterator, function(item) { 23 | var period = item.timePeriod; 24 | var layer = item.layer; 25 | var vcsLogFile = helpers.files.vcsNormalisedLog(period); 26 | return helpers.codeMaat[options.analysis](vcsLogFile, helpers.files.layerGrouping(layer.name)) 27 | .pipe(transformFn(period)) 28 | .pipe(map.obj(function(obj) { 29 | return _.defaults(obj, { name: layer.value }, options.metrics.defaultValue); 30 | })); 31 | }); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/tasks/system_analysis/summary_metrics.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var METRICS = ['revisions', 'commits', 'authors']; 11 | 12 | module.exports = { 13 | selector: function(obj) { 14 | return _.reduce(METRICS, function(metric, key) { 15 | if (obj.stat === key && obj.value) { 16 | metric[key] = obj.value; 17 | } 18 | return metric; 19 | }, {}); 20 | }, 21 | defaultValue: _.reduce(METRICS, function(metric, key) { 22 | metric[key] = 0; 23 | return metric; 24 | }, {}), 25 | accumulatorsMap: _.reduce(METRICS, function(metric, key) { 26 | var metricName = _.camelCase(['cumulative', key]); 27 | metric[metricName] = _.property(key); 28 | return metric; 29 | }, {}) 30 | }; 31 | -------------------------------------------------------------------------------- /lib/utils/arrays.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = { 11 | arrayToFnFactory: function(array, handler) { 12 | return _.map(array, function(item) { 13 | return _.wrap(item, handler); 14 | }); 15 | }, 16 | arrayPairsToObject: function(array) { 17 | var theArray = (array.length % 2) === 0 ? array : _.slice(array, 0, array.length - 1); 18 | return _.fromPairs(_.chunk(theArray, 2)); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/definitions_archive.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = function() { 9 | var definitions = {}; 10 | 11 | this.addDefinition = function(name, definition) { 12 | definitions[name] = definition; 13 | }; 14 | 15 | this.getDefinition = function(name) { 16 | return definitions[name]; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/utils/file_system.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var fs = require('fs'), 9 | _ = require('lodash'), 10 | Bluebird = require('bluebird'); 11 | 12 | var queryEntity = function(queryFn, path) { 13 | return fs.existsSync(path) && fs.statSync(path)[queryFn](); 14 | }; 15 | 16 | module.exports = { 17 | isFile: _.wrap('isFile', queryEntity), 18 | isDirectory: _.wrap('isDirectory', queryEntity), 19 | writeToFile: Bluebird.promisify(fs.writeFile) 20 | }; 21 | -------------------------------------------------------------------------------- /lib/utils/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | DefinitionsArchive: require('./definitions_archive'), 10 | stream: require('./stream'), 11 | json: require('./json'), 12 | arrays: require('./arrays'), 13 | require_ifexists: require('./require_ifexists'), 14 | fileSystem: require('./file_system'), 15 | pathMatchers: require('./path_matchers'), 16 | platformCheck: require('./platform_check'), 17 | SingletonFactory: require('./singleton_factory'), 18 | XmlUtils: require('./xml_utils') 19 | }; 20 | -------------------------------------------------------------------------------- /lib/utils/json.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | fs = require('fs'), 10 | StringDecoder = require('string_decoder').StringDecoder, 11 | Bluebird = require('bluebird'), 12 | map = require('through2-map'), 13 | JSONStream = require('JSONStream'), 14 | multipipe = require('multipipe'); 15 | 16 | var decoder = new StringDecoder(); 17 | 18 | module.exports = { 19 | //TODO: rename objectStreamToFileStream 20 | objectToFileStream: function(filepath, sourceStream) { 21 | return _.tap(fs.createWriteStream(filepath), function(destStream) { 22 | sourceStream.pipe( 23 | multipipe( 24 | map.obj(function(obj) { return JSON.stringify(obj, null, 2) + '\n'; }), 25 | destStream 26 | ) 27 | ); 28 | }); 29 | }, 30 | //TODO: rename arrayStreamToFileStream 31 | objectArrayToFileStream: function(filepath, sourceStream) { 32 | return _.tap(fs.createWriteStream(filepath), function(destStream) { 33 | sourceStream.pipe(multipipe(JSONStream.stringify('[\n', ',\n', '\n]\n'), destStream)); 34 | }); 35 | }, 36 | objectToFile: function(filepath, obj) { 37 | var writeFile = Bluebird.promisify(fs.writeFile); 38 | return writeFile(filepath, JSON.stringify(obj, null, 2)); 39 | }, 40 | fileToObject: function(filepath) { 41 | var readFile = Bluebird.promisify(fs.readFile); 42 | return readFile(filepath) 43 | .then(function(buffer) { 44 | return JSON.parse(decoder.write(buffer)); 45 | }); 46 | }, 47 | fileToObjectStream: function(filepath, parseExpression) { 48 | var pattern = parseExpression || '*'; 49 | return fs.createReadStream(filepath).pipe(JSONStream.parse(pattern)); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /lib/utils/path_matchers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | haveSamePath: function(item1, item2) { 10 | return item1.path === item2.path; 11 | }, 12 | isCoupledWith: function(targetPath, item) { 13 | return item.path === targetPath || item.coupledPath === targetPath; 14 | }, 15 | areCoupled: function(item1, item2) { 16 | return item1.path === item2.path || item1.path.match(item2.coupledPath); 17 | }, 18 | areCoupledWith: function(targetPath, item1, item2) { 19 | return (item1.path.match(item2.coupledPath) && targetPath === item2.path) || 20 | (item1.path.match(item2.path) && targetPath === item2.coupledPath); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/utils/platform_check.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var shell = require('shelljs'), 9 | fs = require('fs'), 10 | ansi = require('ansi-colors'); 11 | 12 | var logErrorAndExit = function(message) { 13 | shell.echo(ansi.red('Platform dependency error\n' + message)); 14 | shell.exit(1); 15 | }; 16 | 17 | module.exports = { 18 | verifyExecutable: function(executable, errorMessage) { 19 | if (!shell.which(executable)) { 20 | logErrorAndExit(errorMessage); 21 | } 22 | }, 23 | verifyPackage: function(shellCmd, expectedOutput, errorMessage) { 24 | var output = shell.exec(shellCmd, { silent: true }).stdout; 25 | if (expectedOutput !== output.trim()) { 26 | logErrorAndExit(errorMessage); 27 | } 28 | }, 29 | verifyFile: function(filePath, errorMessage) { 30 | if (!fs.existsSync(filePath)) { 31 | logErrorAndExit(errorMessage); 32 | } 33 | }, 34 | verifyConfigurationProperty: function(config, prop, errorMessage) { 35 | if (!config.get(prop)) { 36 | logErrorAndExit(errorMessage); 37 | } 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /lib/utils/require_ifexists.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Path = require('path'); 9 | 10 | module.exports = function(currentPath, modulePath) { 11 | var path = Path.join(currentPath, modulePath); 12 | try { 13 | return require(path); 14 | } catch(e) { 15 | if (e instanceof Error && e.code !== 'MODULE_NOT_FOUND') { throw e; } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/utils/singleton_factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(ConstructorFn) { 11 | var instances = []; 12 | 13 | var createObject = function(args) { 14 | var obj = Object.create(ConstructorFn.prototype); 15 | ConstructorFn.apply(obj, args); 16 | return obj; 17 | }; 18 | 19 | this.instance = function() { 20 | var args = _.toArray(arguments); 21 | var instance = _.find(instances, { 'key': args }); 22 | 23 | if (instance === undefined) { 24 | instance = { key: args, object: createObject(arguments) }; 25 | instances.push(instance); 26 | } 27 | return instance.object; 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/utils/stream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | fs = require('fs'), 10 | Bluebird = require('bluebird'), 11 | isStream = require('is-stream'), 12 | through2 = require('through2'), 13 | PassThrough = require('stream').PassThrough; 14 | 15 | module.exports = { 16 | readFileToObjectStream: function(filepath, transformFn) { 17 | return _.tap(new PassThrough({ objectMode: true }), function(outStream) { 18 | fs.readFile(filepath, function(err, data) { 19 | if (err) { 20 | outStream.emit('error', err); 21 | } else { 22 | var outputObject = transformFn(data); 23 | if (_.isObject(outputObject)) { outStream.write(outputObject); } 24 | outStream.end(); 25 | } 26 | }); 27 | }); 28 | }, 29 | reduceToObjectStream: function(transformFn) { 30 | var chunks = []; 31 | return through2.obj( 32 | function(chunk, enc, callback) { 33 | chunks.push(chunk); 34 | callback(); 35 | }, 36 | function(callback) { 37 | if (chunks.length > 0) { 38 | var obj = transformFn(Buffer.concat(chunks, _.reduce(chunks, 'length', 0))); 39 | if (_.isObject(obj)) { this.push(obj); } 40 | } 41 | callback(); 42 | } 43 | ); 44 | }, 45 | streamToPromise: function(stream) { 46 | return new Bluebird(function(resolve, reject) { 47 | if (isStream(stream)) { 48 | _.each(['end', 'finish'], function(eventName) { 49 | stream.on(eventName, resolve); 50 | }); 51 | stream.on('error', reject); 52 | } else { 53 | reject(new Error('Not a stream')); 54 | } 55 | }); 56 | }, 57 | objectStreamToArray: function(stream) { 58 | return new Bluebird(function(resolve, reject) { 59 | var data = []; 60 | stream 61 | .on('data', data.push.bind(data)) 62 | .once('end', resolve.bind(null, data)) 63 | .once('error', reject); 64 | }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /lib/utils/xml_utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = { 11 | nodeWithName: function(name) { 12 | return function(node) { return node.name === name; }; 13 | }, 14 | nodeText: function(node) { 15 | var text = _.find(node.children, function(elem) { return elem.type === 'text'; }) || {}; 16 | return text.value; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/vcs/git/gitlog_stream_transformer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | StringDecoder = require('string_decoder').StringDecoder, 10 | LineStream = require('byline').LineStream, 11 | filter = require('through2-filter'), 12 | map = require('through2-map'); 13 | 14 | module.exports = function(repository, developersInfo) { 15 | var AUTHOR_REGEXP = /^--[a-z0-9]+--[0-9-]+--(.*)$/; 16 | var PATH_REGEXP = /^(\d+|-)\s(\d+|-)\s(.*)$/; 17 | 18 | var decoder = new StringDecoder(); 19 | var normaliseCommitAuthorData = function(line) { 20 | var match = AUTHOR_REGEXP.exec(line); 21 | if (match === null) { return line; } 22 | var authorName = match[1]; 23 | var developer = developersInfo.find(authorName); 24 | return line.replace(authorName, developer.name); 25 | }; 26 | 27 | var validCommitData = function(line, pathEvalCallback) { 28 | var match = PATH_REGEXP.exec(line.trim()); 29 | if (match !== null) { 30 | return _.tap(repository.isValidPath(match[3]), pathEvalCallback || _.noop); 31 | } 32 | return true; 33 | }; 34 | 35 | this.normaliseLogStream = function(inputStream, pathEvalCallback) { 36 | var firstLine = true; 37 | return inputStream 38 | .pipe(new LineStream({ keepEmptyLines: true })) 39 | .pipe(map(function(chunk) { 40 | var line = normaliseCommitAuthorData(decoder.write(chunk)); 41 | if (firstLine) { 42 | firstLine = false; 43 | return line; 44 | } 45 | return '\n' + line; 46 | })) 47 | .pipe(filter(function(chunk) { 48 | return validCommitData(decoder.write(chunk), pathEvalCallback); 49 | })); 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /lib/vcs/git/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | Adapter: require('./git_adapter'), 10 | LogStreamTransformer: require('./gitlog_stream_transformer') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/vcs/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | vcsFactory = require('./vcs_factory'); 10 | 11 | var VcsClient = function(repository) { 12 | var self = this; 13 | var adapter; 14 | 15 | _.each(['revisions', 'showRevisionStream', 'logStream', 'commitMessagesStream'], function(method) { 16 | self[method] = function() { 17 | adapter = adapter || vcsFactory.adapter(repository); 18 | return adapter[method].apply(adapter, arguments); 19 | }; 20 | }); 21 | }; 22 | 23 | var LogTransformer = function(repository, developersInfo) { 24 | var logTransformer; 25 | 26 | this.normaliseLogStream = function() { 27 | logTransformer = logTransformer || vcsFactory.logStreamTransformer(repository, developersInfo); 28 | return logTransformer.normaliseLogStream.apply(logTransformer, arguments); 29 | }; 30 | }; 31 | 32 | 33 | module.exports = { 34 | client: function(repository) { 35 | return new VcsClient(repository); 36 | }, 37 | logTransformer: function(repository, developersInfo) { 38 | return new LogTransformer(repository, developersInfo); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /lib/vcs/svn/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | Adapter: require('./svn_adapter'), 10 | LogStreamTransformer: require('./svnlog_stream_transformer') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/vcs/vcs_factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var appConfig = require('../runtime/app_config'); 9 | 10 | var vcsRepos = { 11 | git: require('./git'), 12 | subversion: require('./svn') 13 | }; 14 | 15 | var getCurrentRepo = function() { 16 | var repo = vcsRepos[appConfig.get('versionControlSystem')]; 17 | if (repo === undefined) { 18 | throw new Error('Cannot find vcs support files for: ' + appConfig.get('versionControlSystem')); 19 | } 20 | return repo; 21 | }; 22 | 23 | module.exports = { 24 | adapter: function(repository) { 25 | var Adapter = getCurrentRepo().Adapter; 26 | 27 | return new Adapter(repository); 28 | }, 29 | logStreamTransformer: function(repository, developersInfo) { 30 | var Transformer = getCurrentRepo().LogStreamTransformer; 31 | 32 | return new Transformer(repository, developersInfo, this.adapter(repository)); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/web/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | (function() { 9 | window.System.config({ 10 | map: { 11 | lodash: '/lib/lodash/lodash.js', 12 | knockout: '/lib/knockout/build/output/knockout-latest.js', 13 | bluebird: '/lib/bluebird/js/browser/bluebird.min.js', 14 | mustache: '/lib/mustache/mustache.min.js', 15 | d3: '/lib/d3/dist/d3.min.js', 16 | 'd3-selection': '/lib/d3-selection/dist/d3-selection.min.js', 17 | 'd3-cloud': '/lib/d3-cloud/build/d3.layout.cloud.js', 18 | 'd3-v6-tip': '/lib/d3-v6-tip/build/d3-v6-tip.min.js' 19 | }, 20 | meta: { 21 | 'd3-cloud': { format: 'global', deps: ['d3'] } 22 | } 23 | }); 24 | window.System.import('/js/main.js').then(function(module) { module.run(); }); 25 | })(); 26 | -------------------------------------------------------------------------------- /lib/web/controllers/report_controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Bluebird = require('bluebird'); 10 | 11 | module.exports = function(graphModels) { 12 | this.selectGraph = function(graphModel) { 13 | _.each(graphModels, function(g) { 14 | g.isSelected(g.id === graphModel.id); 15 | }); 16 | }; 17 | 18 | Bluebird.all(_.invokeMap(_.invokeMap(graphModels, 'initialize'), 'reflect')) 19 | .then(function(promises) { 20 | var selectionIndex; 21 | _.each(promises, function(p, index) { 22 | if (selectionIndex === undefined && p.isFulfilled()) { 23 | selectionIndex = index; 24 | } else if (p.isRejected()) { 25 | //eslint-disable-next-line no-console 26 | console.error('Graph initialization error [' + graphModels[index].id + ']', '-', p.reason()); 27 | } 28 | }); 29 | graphModels[selectionIndex || 0].isSelected(true); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_axis.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'); 10 | 11 | var D3Element = require('./d3_element.js'); 12 | 13 | module.exports = function(parent, definition) { 14 | var axisElement = D3Element.append(parent, definition); 15 | var axisBehavior; 16 | 17 | if (definition.behavior) { 18 | axisBehavior = d3[definition.behavior](); 19 | _.each(definition.settings, function(v, k) { 20 | axisBehavior[k](v); 21 | }); 22 | axisElement.call(axisBehavior); 23 | } 24 | 25 | if (_.isPlainObject(definition.labels)) { 26 | D3Element.applyDefinition(axisElement.selectAll('.tick text'), definition.labels); 27 | } 28 | 29 | this.repaint = function() { 30 | axisElement.call(axisBehavior); 31 | }; 32 | 33 | this.axisBehavior = axisBehavior; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_brush.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'); 10 | 11 | var D3Element = require('./d3_element.js'); 12 | 13 | module.exports = function(parent, definition) { 14 | var self = this; 15 | this.element = D3Element.append(parent, definition); 16 | 17 | switch (definition.orientation) { 18 | case 'horizontal': 19 | this.brushBehavior = d3.brushX(); 20 | break; 21 | case 'vertical': 22 | this.brushBehavior = d3.brushY(); 23 | break; 24 | default: 25 | this.brushBehavior = d3.brush(); 26 | } 27 | 28 | _.each(definition.settings, function(v, k) { 29 | self.brushBehavior[k](v); 30 | }); 31 | 32 | this.element.call(this.brushBehavior); 33 | if (definition.activeSelection) { 34 | this.element.call(this.brushBehavior.move, definition.activeSelection); 35 | } 36 | 37 | this.setActiveSelection = function(range) { 38 | this.element.call(this.brushBehavior.move, range); 39 | }; 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_chart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var D3Element = require('./d3_element.js'), 11 | D3Node = require('./d3_node.js'), 12 | D3Component = require('./d3_component.js'); 13 | 14 | var createSvgContainer = function(chartDefinition, parentContainer) { 15 | var parentElement = parentContainer; 16 | if (chartDefinition.htmlWrapper) { 17 | parentElement = D3Node.createHtmlElement(chartDefinition.htmlWrapper.elementType, parentContainer); 18 | D3Element.applyProperties(parentElement, chartDefinition.htmlWrapper.properties); 19 | } 20 | 21 | return _.tap(D3Node.createSvgElement('svg', parentElement), function(svg) { 22 | D3Element.applyDefinition(svg, chartDefinition); 23 | }); 24 | }; 25 | 26 | module.exports = function(id, chartDefinition) { 27 | var self = this; 28 | var svgContainer = createSvgContainer(chartDefinition, id); 29 | 30 | var components = _.map(_.compact(chartDefinition.components), function(componentDefinition) { 31 | return new D3Component(svgContainer, componentDefinition); 32 | }); 33 | 34 | this.name = chartDefinition.name; 35 | this.svgDocument = svgContainer; 36 | 37 | this.getComponentByName = function(name) { 38 | var c = _.find(components, { 'name': name }); 39 | if (_.isUndefined(c)) { throw new Error('Component "' + name + '" does not exist'); } 40 | return c; 41 | }; 42 | 43 | this.updateComponents = function() { 44 | if (_.isPlainObject(chartDefinition.updateStrategy)) { 45 | _.each(chartDefinition.updateStrategy.components, function(updateDefinition) { 46 | var component = _.find(components, { 'name': updateDefinition.name }); 47 | if (component) { 48 | component[updateDefinition.method](updateDefinition.parameters); 49 | } 50 | }); 51 | } 52 | }; 53 | 54 | _.each(['activate', 'deactivate'], function(fname) { 55 | self[fname] = function() { 56 | _.invokeMap(components, 'componentProxy', [fname]); 57 | }; 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_component.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var D3Element = require('./d3_element.js'), 11 | D3Axis = require('./d3_axis.js'), 12 | D3Brush = require('./d3_brush.js'), 13 | D3Zoom = require('./d3_zoom.js'), 14 | D3Data = require('./d3_data.js'); 15 | 16 | var COMPONENT_TYPES = { 17 | axis: D3Axis, 18 | brush: D3Brush, 19 | zoom: D3Zoom, 20 | data: D3Data 21 | }; 22 | 23 | module.exports = function(container, definition) { 24 | this.name = definition.name; 25 | this.component = new COMPONENT_TYPES[definition.componentType](container, definition); 26 | 27 | this.reset = function() { 28 | this.component.element.remove(); 29 | this.component = new COMPONENT_TYPES[definition.componentType](container, definition); 30 | }; 31 | 32 | this.repaint = function(repaintDefinitions) { 33 | var self = this; 34 | _.each(repaintDefinitions, function(repaintDefinition) { 35 | var element = self.getElement().selectAll(repaintDefinition.elementSelection); 36 | D3Element.applyProperties(element, repaintDefinition.properties); 37 | }); 38 | }; 39 | 40 | this.getElement = function() { 41 | return this.component.element; 42 | }; 43 | 44 | this.componentProxy = function(fname, parameters) { 45 | if (_.isFunction(this.component[fname])) { 46 | this.component[fname].apply(this.component, parameters); 47 | } 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var D3Element = require('./d3_element.js'), 11 | D3Tooltip = require('./d3_tooltip.js'); 12 | 13 | var D3Data = function(parent, definition) { 14 | var container = D3Element.append(parent, definition); 15 | var dataElementSelection = container 16 | .selectAll('g') 17 | .data(definition.series) 18 | .enter() 19 | .append('g'); 20 | var tooltip = null; 21 | if (_.isArray(definition.subDataElements)) { 22 | _.each(definition.subDataElements, function(subDataElement) { 23 | new D3Data(dataElementSelection, subDataElement); 24 | }); 25 | } 26 | _.each(definition.graphicElements, function(graphicElementDefinition) { 27 | D3Element.append(dataElementSelection, graphicElementDefinition); 28 | if (_.isPlainObject(graphicElementDefinition.tooltip)) { 29 | tooltip = new D3Tooltip(dataElementSelection, graphicElementDefinition.elementType, graphicElementDefinition.tooltip); 30 | } 31 | }); 32 | 33 | var toggleTooltip = function(enabled) { 34 | if (tooltip) { 35 | enabled ? tooltip.enable() : tooltip.disable(); 36 | } 37 | }; 38 | 39 | this.element = container; 40 | 41 | this.activate = function() { 42 | toggleTooltip(true); 43 | }; 44 | 45 | this.deactivate = function() { 46 | toggleTooltip(false); 47 | }; 48 | }; 49 | 50 | module.exports = D3Data; 51 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_element.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var D3Transform = require('./d3_transform.js'); 11 | 12 | module.exports = { 13 | applyProperties: function(d3Element, properties) { 14 | var props = properties || {}; 15 | new D3Transform() 16 | .withOffset(props.offset) 17 | .withRotation(props.rotation) 18 | .applyToElement(d3Element); 19 | 20 | _.each(props.attributes, function(v, k) { d3Element.attr(k, v); }); 21 | _.each(props.style, function(v, k) { d3Element.style(k, v); }); 22 | _.each(['text', 'html'], function(fn) { 23 | if (props[fn]) { d3Element[fn](props[fn]); } 24 | }); 25 | }, 26 | applyInnerElements: function(d3Element, innerElements) { 27 | var self = this; 28 | _.each(innerElements, function(elementDefinition) { 29 | self.append(d3Element, elementDefinition); 30 | }); 31 | }, 32 | applyDefinition: function(d3Element, definition) { 33 | this.applyProperties(d3Element, definition.properties); 34 | this.applyInnerElements(d3Element, definition.innerElements); 35 | }, 36 | append: function(parent, definition) { 37 | var self = this; 38 | return _.tap(parent.append(definition.elementType || 'g'), function(d3Element) { 39 | self.applyDefinition(d3Element, definition); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_node.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'); 10 | 11 | var appendChildElement = function(parentElement, childElement) { 12 | var parent = parentElement; 13 | if (_.isString(parentElement)) { 14 | parent = d3.select('#' + parentElement); 15 | } 16 | parent.append(function() { return childElement.node(); }); 17 | }; 18 | 19 | module.exports = { 20 | createHtmlElement: function(htmlTag, parent) { 21 | return _.tap(d3.create(htmlTag), function(htmlElement) { 22 | appendChildElement(parent, htmlElement); 23 | }); 24 | }, 25 | createSvgElement: function(svgTag, parent) { 26 | return _.tap(d3.select(document.createElementNS(d3.namespaces.svg, svgTag)), function(d3Element) { 27 | appendChildElement(parent, d3Element); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_tooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | d3Tip = require('d3-v6-tip'), 10 | _ = require('lodash'); 11 | 12 | module.exports = function(container, targetSelector, definition) { 13 | var TOOLTIP_CLASS = 'd3-tip'; 14 | var DEFAULT_ACTIONS = { show: { event: 'mouseover' }, hide: { event: 'mouseout' } }; 15 | var tip = d3Tip.tip(); 16 | 17 | var getTooltipElement = function() { 18 | return d3.select('.' + TOOLTIP_CLASS); 19 | }; 20 | 21 | tip.attr('class', TOOLTIP_CLASS); 22 | _.each(['offset', 'direction', 'html'], function(prop) { 23 | if (definition[prop]) { tip[prop](definition[prop]); } 24 | }); 25 | 26 | container.call(tip); 27 | var actions = _.assign({}, DEFAULT_ACTIONS, definition.actions); 28 | _.each(actions, function(actionDefinition, actionName) { 29 | container.selectAll(targetSelector) 30 | .on(actionDefinition.event, function(event, d) { 31 | var doAction = _.isFunction(actionDefinition.condition) ? actionDefinition.condition(d) : true; 32 | if (doAction) { 33 | tip[actionName](event, d, this); 34 | event.stopPropagation(); 35 | } 36 | }); 37 | }); 38 | 39 | this.enable = function() { 40 | if (definition.class) { 41 | getTooltipElement().classed(definition.class, true); 42 | } 43 | }; 44 | 45 | this.disable = function() { 46 | if (definition.class && definition.allowDisable) { 47 | getTooltipElement().classed(definition.class, false); 48 | } 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_transform.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var TRANSLATE_DEFAULT = { x: 0, y: 0 }; 11 | 12 | module.exports = function() { 13 | var transforms = []; 14 | 15 | this.withOffset = function(value) { 16 | if (_.isPlainObject(value)) { 17 | var offset = _.defaults({}, value, TRANSLATE_DEFAULT); 18 | transforms.push('translate(' + [offset.x, offset.y].join(',') + ')'); 19 | } else if (_.isFunction(value)) { 20 | transforms.push(function() { 21 | var offset = _.defaults(value.apply(null, arguments), TRANSLATE_DEFAULT); 22 | return 'translate(' + [offset.x, offset.y].join(',') + ')'; 23 | }); 24 | } 25 | return this; 26 | }; 27 | 28 | this.withRotation = function(value) { 29 | if (_.isNumber(value)) { 30 | transforms.push('rotate(' + value + ')'); 31 | } else if (_.isFunction(value)) { 32 | transforms.push(function() { 33 | return 'rotate(' + _.spread(value)(arguments) + ')'; 34 | }); 35 | } 36 | return this; 37 | }; 38 | 39 | this.applyToElement = function(d3Element) { 40 | if (!_.isEmpty(transforms)) { 41 | d3Element.attr('transform', function() { 42 | var args = arguments; 43 | return _.map(transforms, function(value) { 44 | if (_.isFunction(value)) { return value.apply(null, args); } 45 | return value; 46 | }).join(''); 47 | }); 48 | } 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/web/d3_chart_components/d3_zoom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'); 10 | 11 | var D3Element = require('./d3_element.js'); 12 | 13 | module.exports = function(parent, definition) { 14 | var self = this; 15 | this.element = D3Element.append(parent, definition); 16 | this.zoomBehavior = d3.zoom(); 17 | 18 | _.each(definition.settings, function(v, k) { 19 | self.zoomBehavior[k](v); 20 | }); 21 | 22 | this.element.call(this.zoomBehavior); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/web/diagrams/bubble_chart/circle_select_handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = function() { 11 | this.bindTo = function(charts, model) { 12 | var mainChart = _.find(charts, { 'name': 'main' }); 13 | var allNodes = mainChart 14 | .getComponentByName('node-data') 15 | .getElement() 16 | .selectAll('circle'); 17 | 18 | allNodes.on('click', function(event, node) { 19 | model.selectNode(node); 20 | mainChart.updateComponents(); 21 | event.stopPropagation(); 22 | }); 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /lib/web/diagrams/bubble_chart/pack_layout_adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | Bluebird = require('bluebird'), 10 | _ = require('lodash'); 11 | 12 | module.exports = function(options) { 13 | var packLayout = d3.pack() 14 | .padding(2) 15 | .size([options.diameter, options.diameter]); 16 | 17 | var normaliseData = function(data) { 18 | _.each(data.children, function(node) { 19 | node.name = _.last(node[options.nameProperty].split('/')); 20 | }); 21 | }; 22 | 23 | this.toSeries = Bluebird.method(function(data) { 24 | if (_.isEmpty(data.children)) { 25 | return null; 26 | } 27 | normaliseData(data); 28 | 29 | var rootNode = d3.hierarchy(data); 30 | rootNode.sum(function(node) { 31 | return node[options.valueProperty]; 32 | }); 33 | packLayout(rootNode); 34 | 35 | return rootNode.descendants(); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/web/diagrams/controls_proxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | ko = require('knockout'); 10 | 11 | module.exports = function(controlsConfiguration) { 12 | var groupedInstances = function(controls) { 13 | return _.reduce(_.groupBy(_.values(controls), 'group'), function(acc, ctrls, group) { 14 | var availableControls = _.filter(_.map(ctrls, 'instance'), _.method('hasData')); 15 | if (availableControls.length > 0) { acc[group] = availableControls; } 16 | return acc; 17 | }, {}); 18 | }; 19 | 20 | var initializeControls = function(controls, series, callback) { 21 | _.each(controls, function(ctrl) { 22 | var ctrlData = (ctrl.dataTransform || _.identity).call(null, series); 23 | ctrl.instance.init(ctrlData); 24 | if (_.isFunction(callback)) { callback.call(null, ctrl); } 25 | }); 26 | return groupedInstances(controls); 27 | }; 28 | 29 | this.hasFilters = ko.observable(false); 30 | this.hasWidgets = ko.observable(false); 31 | 32 | this.initializeFilters = function(series, model) { 33 | return initializeControls(controlsConfiguration.filters, series, function(filter) { 34 | filter.instance.outputValue.subscribe(function() { 35 | model.applyFilters(controlsConfiguration.filters, filter); 36 | }); 37 | }); 38 | }; 39 | 40 | this.initializeWidgets = function(series) { 41 | return initializeControls(controlsConfiguration.widgets, series); 42 | }; 43 | 44 | this.initialize = function(series, model) { 45 | this.filters = this.initializeFilters(series, model); 46 | this.hasFilters(!_.isEmpty(this.filters)); 47 | this.widgets = this.initializeWidgets(series); 48 | this.hasWidgets(!_.isEmpty(this.widgets)); 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/web/diagrams/data_proxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Bluebird = require('bluebird'), 9 | _ = require('lodash'); 10 | 11 | var defaultLayoutAdapter = { toSeries: Bluebird.resolve }; 12 | 13 | module.exports = function(layoutAdapter, dataTransform) { 14 | var adapter = layoutAdapter || defaultLayoutAdapter; 15 | var transformFn = dataTransform || _.identity; 16 | 17 | this.processData = function(data) { 18 | return adapter.toSeries(data).then(transformFn); 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/web/diagrams/diagram.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | ko = require('knockout'); 10 | 11 | var GraphPainter = require('./graph_painter.js'), 12 | ControlsProxy = require('./controls_proxy.js'), 13 | DataProxy = require('./data_proxy.js'); 14 | 15 | module.exports = function(id, diagramSettings) { 16 | var graphPainter = new GraphPainter(id, diagramSettings.graphHandlers); 17 | var dataProxy = new DataProxy(diagramSettings.layoutAdapter, diagramSettings.dataTransform); 18 | 19 | this.controls = new ControlsProxy(diagramSettings.controls || {}); 20 | this.cssClass = diagramSettings.configuration.style.cssClass; 21 | this.hasData = ko.observable(false); 22 | 23 | this.onData = function(data) { 24 | var self = this; 25 | dataProxy.processData(data).then(function(series) { 26 | self.hasData((!_.isEmpty(series))); 27 | if (!self.hasData()) { return; } 28 | 29 | var model = new diagramSettings.Model(diagramSettings.configuration, series); 30 | 31 | self.controls.initialize(series, model); 32 | 33 | graphPainter.draw(model); 34 | }); 35 | }; 36 | 37 | this.activate = function() { 38 | _.invokeMap(graphPainter.charts, 'activate'); 39 | }; 40 | 41 | this.deactivate = function() { 42 | _.invokeMap(graphPainter.charts, 'deactivate'); 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/web/diagrams/enclosure_chart/clipboard_handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var ClipboardHelper = require('../../helpers/clipboard_helper.js'); 11 | 12 | module.exports = function(options) { 13 | this.bindTo = function(charts) { 14 | var mainChart = _.find(charts, { 'name': 'main' }); 15 | var allLeafNodes = mainChart 16 | .getComponentByName('node-data') 17 | .getElement() 18 | .selectAll('circle') 19 | .filter(_.method('isLeaf')); 20 | 21 | allLeafNodes.on('dblclick', ClipboardHelper.nodeEventHandler(options)); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /lib/web/diagrams/enclosure_chart/colored_node_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var BaseNodeHelper = require('./base_node_helper.js'); 11 | 12 | var NodeHelper = function(config) { 13 | BaseNodeHelper.call(this, config); 14 | }; 15 | 16 | NodeHelper.prototype = Object.create(BaseNodeHelper.prototype); 17 | 18 | _.extend(NodeHelper.prototype, { 19 | nodeColorValue: function(node) { 20 | return node.data[this.config.series.colorProperty]; 21 | } 22 | }); 23 | 24 | module.exports = NodeHelper; 25 | -------------------------------------------------------------------------------- /lib/web/diagrams/enclosure_chart/pack_layout_adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | Bluebird = require('bluebird'), 10 | _ = require('lodash'); 11 | 12 | var NodeMixin = { 13 | fullName: function() { 14 | return _.compact(_.map(_.reverse(this.ancestors()), function(node) { 15 | return node.data.name; 16 | })).join('/'); 17 | }, 18 | hasLayout: function() { 19 | return _.isNumber(this.r) && _.isNumber(this.x) && _.isNumber(this.y); 20 | }, 21 | isRoot: function() { 22 | return _.isUndefined(this.parent); 23 | }, 24 | isLeaf: function() { 25 | return _.isEmpty(this.children); 26 | } 27 | }; 28 | 29 | module.exports = function(options) { 30 | var packLayout = d3.pack() 31 | .padding(2) 32 | .size([options.diameter, options.diameter]); 33 | 34 | this.toSeries = Bluebird.method(function(data) { 35 | if (_.isEmpty(data.children)) { 36 | return null; 37 | } 38 | var rootNode = d3.hierarchy(data); 39 | rootNode.sum(function(node) { 40 | return _.ceil(node[options.valueProperty]); 41 | }); 42 | 43 | packLayout(rootNode); 44 | 45 | return _.filter( 46 | _.map(rootNode.descendants(), function(node) { return _.mixin(node, NodeMixin); }), 47 | function(node) { return node.hasLayout(); }); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/web/diagrams/enclosure_chart/weighted_node_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var BaseNodeHelper = require('./base_node_helper.js'); 11 | 12 | var NodeHelper = function(config) { 13 | BaseNodeHelper.call(this, config); 14 | }; 15 | 16 | NodeHelper.prototype = Object.create(BaseNodeHelper.prototype); 17 | 18 | _.extend(NodeHelper.prototype, { 19 | nodeHiglighted: function(node) { 20 | return this.config.style.nodeHighlight && node.fullName() === this.config.style.nodeHighlight.name; 21 | }, 22 | nodeWeight: function(node) { 23 | return node.data[this.config.series.calculatedWeightProperty]; 24 | }, 25 | circleNodeFill: function(node) { 26 | if (this.nodeHiglighted(node)) { return this.config.style.nodeHighlight.color; } 27 | if (this.nodeWeight(node) > 0.0) { return this.config.style.weightedNodeColor; } 28 | 29 | return BaseNodeHelper.prototype.circleNodeFill.call(this, node); 30 | }, 31 | circleNodeOpacity: function(node) { 32 | if (this.nodeHiglighted(node)) { return 1; } 33 | return this.nodeWeight(node); 34 | }, 35 | textNodeClass: function(node) { 36 | var labelClasses = BaseNodeHelper.prototype.textNodeClass.call(this, node); 37 | if (node.isLeaf() && this.nodeWeight(node) > 0.4) { 38 | labelClasses = [labelClasses, 'label-heavy'].join(' '); 39 | } 40 | return labelClasses; 41 | } 42 | }); 43 | 44 | module.exports = NodeHelper; 45 | -------------------------------------------------------------------------------- /lib/web/diagrams/graph_painter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var D3Chart = require('../d3_chart_components/d3_chart.js'); 11 | 12 | module.exports = function(svgContainerSelector, graphHandlers) { 13 | var self = this; 14 | this.charts = []; 15 | 16 | this.draw = function(model) { 17 | this.charts = _.map(model.chartDefinitions, function(definition) { 18 | return new D3Chart(svgContainerSelector, definition); 19 | }); 20 | 21 | if (_.isFunction(model.onModelChange)) { 22 | model.onModelChange(function() { _.invokeMap(self.charts, 'updateComponents'); }); 23 | } 24 | 25 | _.each(graphHandlers, function(handler) { 26 | handler.bindTo(self.charts, model); 27 | }); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/web/diagrams/line_chart/clipboard_handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var ClipboardHelper = require('../../helpers/clipboard_helper.js'); 11 | 12 | module.exports = function(options) { 13 | this.bindTo = function(charts) { 14 | var mainChart = _.find(charts, { 'name': 'main' }); 15 | var allDots = mainChart 16 | .getComponentByName('dots-' + options.seriesName) 17 | .getElement() 18 | .selectAll('circle'); 19 | 20 | allDots.on('dblclick', ClipboardHelper.nodeEventHandler(options)); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /lib/web/diagrams/line_chart/diagram_support.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'); 10 | 11 | var ScaleDomainFactory = require('../../utils/scale_domain_factory.js'); 12 | 13 | module.exports = function(config) { 14 | var seriesConfig = config.series; 15 | 16 | var reduceAxisData = function(seriesGroups, axis) { 17 | return _.reduce(seriesGroups, function(values, group) { 18 | var allValues = values.concat(_.map(group.allValues(), seriesConfig[axis].valueProperty)); 19 | return _.uniqBy(allValues, seriesConfig[axis].valueCompareFn || _.identity); 20 | }, []); 21 | }; 22 | 23 | this.createPlotLine = function(scale) { 24 | var line = d3.line().curve(config.plotLine.curve) 25 | .x(function(dataPoint) { return scale.x(dataPoint[seriesConfig.x.valueProperty]); }) 26 | .y(function(dataPoint) { return scale.y(dataPoint[seriesConfig.y.valueProperty]); }); 27 | 28 | return function(data) { return line(data.allValues()); }; 29 | }; 30 | 31 | this.createScale = function(seriesGroups, width, height) { 32 | var dataArrayX = reduceAxisData(seriesGroups, 'x'); 33 | var dataArrayY = reduceAxisData(seriesGroups, 'y'); 34 | return { 35 | x: seriesConfig.x.scale().range([0, width]).domain(ScaleDomainFactory(dataArrayX, seriesConfig.x.domainFactory)), 36 | y: seriesConfig.y.scale().range([height, 0]).domain(ScaleDomainFactory(dataArrayY, seriesConfig.y.domainFactory)) 37 | }; 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/web/diagrams/line_chart/legend_model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = function(config, series, colorScale) { 9 | var elementsOffset = function(d, i) { return { x: 10, y: i * 15 }; }; 10 | 11 | this.legendDefinition = { 12 | name: 'legend', 13 | componentType: 'data', 14 | properties: { 15 | offset: { x: config.style.width - config.style.margin.right, y: config.style.margin.top }, 16 | attributes: { class: 'legend' } 17 | }, 18 | series: series, 19 | graphicElements: [ 20 | { 21 | elementType: 'circle', 22 | properties: { 23 | offset: elementsOffset, 24 | attributes: { 25 | r: 5, 26 | cx: 30, 27 | cy: 9 28 | }, 29 | style: { fill: colorScale } 30 | } 31 | }, 32 | { 33 | elementType: 'text', 34 | properties: { 35 | offset: elementsOffset, 36 | attributes: { 37 | x: 20, 38 | y: 9, 39 | dy: '.35em' 40 | }, 41 | text: function(d) { return d.name; } 42 | } 43 | } 44 | ] 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/web/diagrams/line_chart/zoom_brush_handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'); 10 | 11 | module.exports = function(options) { 12 | this.bindTo = function(charts, model) { 13 | var mainChart = _.find(charts, { 'name': 'main' }); 14 | var brushPanelChart = _.find(charts, { 'name': 'brushPanel' }); 15 | 16 | var mainChartXAxis = mainChart.getComponentByName('xAxis').component; 17 | var mainChartZoom = mainChart.getComponentByName('zoom').component; 18 | var brushPanelChartXAxis = brushPanelChart.getComponentByName('xAxis').component; 19 | var brushPanelChartBrush = brushPanelChart.getComponentByName('brush').component; 20 | 21 | var diagramXScale = mainChartXAxis.axisBehavior.scale(); 22 | var brushXScale = brushPanelChartXAxis.axisBehavior.scale(); 23 | 24 | mainChartZoom.zoomBehavior.on('zoom', function(event) { 25 | if (!event.sourceEvent) return; 26 | var t = event.transform; 27 | diagramXScale.domain(t.rescaleX(brushXScale).domain()); 28 | mainChart.updateComponents(); 29 | mainChartXAxis.repaint(); 30 | brushPanelChartBrush.setActiveSelection(diagramXScale.range().map(t.invertX, t)); 31 | }); 32 | brushPanelChartBrush.brushBehavior.on('brush end', function(event) { 33 | if (!event.sourceEvent) return; 34 | var s = event.selection || model.scale.x2.range(); 35 | diagramXScale.domain(s.map(brushXScale.invert, brushXScale)); 36 | mainChart.updateComponents(); 37 | mainChartXAxis.repaint(); 38 | mainChartZoom.element.call( 39 | mainChartZoom.zoomBehavior.transform, 40 | d3.zoomIdentity.scale(options.zoomWidth / (s[1] - s[0])).translate(-s[0], 0) 41 | ); 42 | }); 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/web/diagrams/network_graph/force_drag.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'); 9 | 10 | module.exports = function(simulation) { 11 | var dragstarted = function(event, d) { 12 | if (!event.active) { 13 | simulation.alphaTarget(0.3).restart(); 14 | } 15 | d.fx = d.x; 16 | d.fy = d.y; 17 | }; 18 | 19 | var dragged = function(event, d) { 20 | d.fx = event.x; 21 | d.fy = event.y; 22 | }; 23 | 24 | var dragended = function(event, d) { 25 | if (!event.active) { 26 | simulation.alphaTarget(0); 27 | } 28 | d.fx = null; 29 | d.fy = null; 30 | }; 31 | 32 | this.bindTo = function(nodes) { 33 | nodes.call(d3.drag() 34 | .on('start', dragstarted) 35 | .on('drag', dragged) 36 | .on('end', dragended)); 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /lib/web/diagrams/network_graph/force_simulation_handler.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | d3 = require('d3'); 10 | 11 | var ForceDrag = require('./force_drag.js'); 12 | 13 | module.exports = function(options) { 14 | var simulation = d3.forceSimulation() 15 | .force('link', d3.forceLink() 16 | .id(function(node) { return node[options.nodeIdProperty]; }) 17 | .distance(Math.min(options.width, options.height) / 2) 18 | .strength(function(link) { return 1 / link[options.linkStrengthFactorProperty]; }) 19 | ) 20 | .force('charge', d3.forceManyBody() 21 | .strength(function() { return -200; }) 22 | ) 23 | .force('center', d3.forceCenter(options.width / 2, options.height / 2)); 24 | 25 | this.bindTo = function(charts, model) { 26 | var mainChart = _.find(charts, { 'name': 'main' }); 27 | 28 | simulation.nodes(model.graphData.nodes); 29 | simulation.force('link').links(model.graphData.links); 30 | 31 | var allNodes = mainChart.getComponentByName('node-data').getElement().selectAll('circle'); 32 | var allLinks = mainChart.getComponentByName('link-data').getElement().selectAll('line'); 33 | 34 | if (options.allowDrag) { 35 | new ForceDrag(simulation).bindTo(allNodes); 36 | } 37 | 38 | simulation.on('tick', function() { 39 | allNodes 40 | .attr('cx', function(node) { return Math.max(options.nodeRadius, Math.min(options.width - options.nodeRadius, node.x)); }) 41 | .attr('cy', function(node) { return Math.max(options.nodeRadius, Math.min(options.height - options.nodeRadius, node.y)); }); 42 | allLinks 43 | .attr('x1', function(link) { return link.source.x; }) 44 | .attr('y1', function(link) { return link.source.y; }) 45 | .attr('x2', function(link) { return link.target.x; }) 46 | .attr('y2', function(link) { return link.target.y; }); 47 | }); 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/web/diagrams/treemap/treemap_layout_adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'), 9 | _ = require('lodash'), 10 | Bluebird = require('bluebird'); 11 | 12 | module.exports = function(options) { 13 | var treemap = d3.treemap() 14 | .size([options.width, options.height]) 15 | .round(false); 16 | 17 | 18 | this.toSeries = Bluebird.method(function(data) { 19 | if (_.isEmpty(data.children)) { 20 | return null; 21 | } 22 | var rootNode = d3.hierarchy(data).sum(function(node) { 23 | return node[options.valueProperty]; 24 | }); 25 | treemap(rootNode); 26 | 27 | return rootNode.descendants(); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/web/diagrams/word_cloud/cloud_layout_adapter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3Cloud = require('d3-cloud'), 9 | Bluebird = require('bluebird'), 10 | _ = require('lodash'); 11 | 12 | module.exports = function(options) { 13 | var layout = d3Cloud.layout.cloud(); 14 | 15 | this.toSeries = function(data) { 16 | var maxCount = _.maxBy(data, function(word) { return word.count; }).count; 17 | 18 | var series = _.map(data, function(word) { 19 | return { text: word.text, count: word.count, size: word.count/maxCount * 100 }; 20 | }); 21 | 22 | return new Bluebird(function(resolve) { 23 | layout.words(series) 24 | .size([options.width, options.height]) 25 | .fontSize(function(word) { return word.size; }) 26 | .padding(options.wordPadding) 27 | .on('end', resolve) 28 | .start(); 29 | }); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /lib/web/filters/base_filter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'); 9 | 10 | var Filter = function() { 11 | this.hasData = ko.observable(false); 12 | this.inputValue = ko.observable(); 13 | this.outputValue = this.inputValue; 14 | this.displayValue = this.inputValue; 15 | }; 16 | 17 | Filter.prototype.init = function() {}; 18 | 19 | module.exports = Filter; 20 | -------------------------------------------------------------------------------- /lib/web/filters/color_range.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'), 9 | _ = require('lodash'); 10 | 11 | var BaseFilter = require('./base_filter.js'); 12 | 13 | var Filter = function(colorScaleFactory) { 14 | BaseFilter.call(this); 15 | this.colorScaleFactory = colorScaleFactory; 16 | }; 17 | 18 | Filter.prototype = Object.create(BaseFilter.prototype); 19 | 20 | Filter.prototype.init = function(valuesArray) { 21 | var colorScale = this.colorScaleFactory(valuesArray); 22 | 23 | this.hasData(valuesArray.length > 1); 24 | this.colorMap = _.map(valuesArray, function(value) { 25 | return { name: value, color: colorScale(value), isVisible: ko.observable(true) }; 26 | }); 27 | 28 | this.select = function(data) { 29 | if (_.every(this.colorMap, _.method('isVisible'))) { 30 | var nonSelectedItems = _.filter(this.colorMap, function(item) { return item.name !== data.name; }); 31 | _.each(nonSelectedItems, function(item) { item.isVisible(false); }); 32 | this.outputValue(data.name); 33 | } else { 34 | _.each(this.colorMap, function(item) { item.isVisible(true); }); 35 | this.outputValue(null); 36 | } 37 | }; 38 | }; 39 | 40 | module.exports = Filter; 41 | -------------------------------------------------------------------------------- /lib/web/filters/group_list_selection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'), 9 | _ = require('lodash'); 10 | 11 | var BaseFilter = require('./base_filter.js'); 12 | 13 | var Filter = function(groupKey) { 14 | BaseFilter.call(this); 15 | this.groupKey = groupKey; 16 | }; 17 | 18 | Filter.prototype = Object.create(BaseFilter.prototype); 19 | 20 | Filter.prototype.init = function(valuesArray) { 21 | this.listValues = _.map(valuesArray, function(value) { 22 | return { name: value, isSelected: ko.observable(true) }; 23 | }); 24 | 25 | this.hasData(valuesArray.length > 1); 26 | 27 | this.select = function(data) { 28 | var item = _.find(this.listValues, { 'name': data.name }); 29 | item.isSelected(!item.isSelected()); 30 | var allSelectedItems = _.filter(this.listValues, _.method('isSelected')); 31 | this.outputValue(_.map(allSelectedItems, 'name')); 32 | }; 33 | }; 34 | 35 | module.exports = Filter; 36 | -------------------------------------------------------------------------------- /lib/web/filters/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | RegExpValue: require('./regexp.js'), 10 | MetricRange: require('./metric_range.js'), 11 | PercentageMetricRange: require('./percentage_metric_range.js'), 12 | ColorRange: require('./color_range.js'), 13 | GroupListSelection: require('./group_list_selection.js') 14 | }; 15 | -------------------------------------------------------------------------------- /lib/web/filters/metric_range.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'), 9 | _ = require('lodash'); 10 | 11 | var BaseFilter = require('./base_filter.js'); 12 | 13 | var Filter = function() { 14 | BaseFilter.call(this); 15 | 16 | this.defineMinMax = true; 17 | this.range = ko.observable({ min: 0, max: 0 }); 18 | }; 19 | 20 | Filter.prototype = Object.create(BaseFilter.prototype); 21 | Filter.prototype.init = function(valuesArray) { 22 | this.hasData(valuesArray.length > 1); 23 | this.range({ min: _.min(valuesArray), max: _.max(valuesArray) }); 24 | this.inputValue(this.range().min); 25 | }; 26 | 27 | module.exports = Filter; 28 | -------------------------------------------------------------------------------- /lib/web/filters/percentage_metric_range.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'); 9 | 10 | var MetricRangeFilter = require('./metric_range.js'); 11 | 12 | var Filter = function() { 13 | var self = this; 14 | MetricRangeFilter.call(this); 15 | 16 | this.defineMinMax = false; 17 | 18 | this.displayValue = ko.pureComputed(function() { 19 | return Math.round(self.range().max * parseInt(self.inputValue())); 20 | }); 21 | this.outputValue = ko.pureComputed(function() { 22 | return self.range().max * parseInt(self.inputValue()) / 100; 23 | }); 24 | }; 25 | 26 | Filter.prototype = Object.create(MetricRangeFilter.prototype); 27 | Filter.prototype.init = function() { 28 | MetricRangeFilter.prototype.init.call(this, [0, 1]); 29 | }; 30 | 31 | module.exports = Filter; 32 | -------------------------------------------------------------------------------- /lib/web/filters/regexp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'); 9 | 10 | var BaseFilter = require('./base_filter.js'); 11 | 12 | var Filter = function() { 13 | var self = this; 14 | BaseFilter.call(this); 15 | 16 | this.outputValue = ko.pureComputed(function() { 17 | try { 18 | return new RegExp(self.inputValue()); 19 | } catch(e) { 20 | return self.inputValue(); 21 | } 22 | }); 23 | }; 24 | 25 | Filter.prototype = Object.create(BaseFilter.prototype); 26 | Filter.prototype.init = function() { 27 | this.hasData(true); 28 | }; 29 | 30 | module.exports = Filter; 31 | -------------------------------------------------------------------------------- /lib/web/helpers/clipboard_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var copyToClipboard = function(text) { 11 | var handler = function(event) { 12 | event.clipboardData.setData('text/plain', text); 13 | document.removeEventListener('copy', handler, true); 14 | event.preventDefault(); 15 | }; 16 | 17 | document.addEventListener('copy', handler, true); 18 | document.execCommand('copy'); 19 | }; 20 | 21 | module.exports = { 22 | nodeEventHandler: function(options) { 23 | return function(event, node) { 24 | var text = _.isFunction(options.text) ? options.text(node) : options.text; 25 | copyToClipboard(text); 26 | if (options.message) { window.alert(options.message); } //eslint-disable-line no-alert 27 | event.stopPropagation(); 28 | }; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/web/helpers/mustache_helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var mustache = require('mustache'), 9 | Bluebird = require('bluebird'); 10 | 11 | var asyncLoader = require('../utils/async_loader.js'); 12 | 13 | var parsedTemplates = {}; 14 | var domParser = new DOMParser(); 15 | 16 | var parseTemplate = function(id, content) { 17 | var doc = domParser.parseFromString(content, 'text/html'); 18 | var template = doc.getElementById(id).textContent; 19 | mustache.parse(template); 20 | return template; 21 | }; 22 | 23 | module.exports = { 24 | renderTemplate: function(id, data) { 25 | return mustache.render(parsedTemplates[id], data); 26 | }, 27 | loadTemplate: function(tmpl) { 28 | if (parsedTemplates[tmpl.id]) { return Bluebird.resolve(); } 29 | 30 | return asyncLoader.loadTemplate(tmpl.file).then(function(content) { 31 | if (parsedTemplates[tmpl.id]) { return; } 32 | 33 | parsedTemplates[tmpl.id] = parseTemplate(tmpl.id, content); 34 | }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/web/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'); 9 | 10 | var ViewModel = require('./view_model.js'), 11 | QueryParameter = require('./utils/query_parameter.js'); 12 | 13 | module.exports.run = function() { 14 | window.viewModel = new ViewModel(QueryParameter.fromRequestUrl()); //for debugging purposes 15 | ko.applyBindings(window.viewModel); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/web/models/graph_control_group.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | Bluebird = require('bluebird'), 10 | ko = require('knockout'); 11 | 12 | var asyncLoader = require('../utils/async_loader.js'); 13 | 14 | module.exports = function() { 15 | var self = this; 16 | var templates = []; 17 | this.templateIds = ko.observable(); 18 | 19 | this.addTemplate = function(templateProperties, templateData) { 20 | templates.push({ properties: templateProperties, data: templateData }); 21 | }; 22 | 23 | this.hasTemplates = function() { 24 | return templates.length > 0; 25 | }; 26 | 27 | this.loadAllTemplates = function() { 28 | return Bluebird.all(_.map(templates, function(tmpl) { 29 | return asyncLoader.loadTemplateIntoDocument(tmpl.properties.id, tmpl.properties.file); 30 | })).then(function() { 31 | self.templateIds(_.map(templates, 'properties.id')); 32 | }); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /lib/web/models/manifest_mixin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = { 11 | getReportUrl: function() { 12 | return '?reportId=' + this.id; 13 | }, 14 | parseDateRange: function() { 15 | var dates = this.dateRange.split('_'); 16 | return { from: dates[0], to: dates[1] }; 17 | }, 18 | selectAvailableGraphs: function(graphs) { 19 | var enabledDiagrams = this.enabledDiagrams; 20 | return _.filter(graphs, function(g) { 21 | return _.includes(enabledDiagrams, g.diagramName); 22 | }); 23 | }, 24 | hasLayers: function() { 25 | return _.has(this.parameters, 'layerGroup'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /lib/web/models/report_model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var GraphModel = require('./graph_model.js'), 11 | ManifestMixin = require('./manifest_mixin.js'), 12 | reports = require('../reports/index.js'); 13 | 14 | var ReportModel = function(configuration) { 15 | this.metadata = configuration.metadata; 16 | this.graphModels = _.filter( 17 | _.map(configuration.graphModels, GraphModel.create), 18 | 'hasDataFile' 19 | ); 20 | 21 | this.hasMultipleGraphs = this.graphModels.length > 1; 22 | this.hasGraphs = this.graphModels.length > 0; 23 | }; 24 | 25 | ReportModel.create = function(manifest) { 26 | var reportConfiguration = reports[manifest.reportName](_.mixin(manifest, ManifestMixin)); 27 | return new ReportModel(reportConfiguration); 28 | }; 29 | 30 | module.exports = ReportModel; 31 | -------------------------------------------------------------------------------- /lib/web/models/reports_list_model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'), 9 | _ = require('lodash'); 10 | 11 | var asyncLoader = require('../utils/async_loader.js'), 12 | localeDetection = require('../utils/locale_detection.js'), 13 | ManifestMixin = require('./manifest_mixin.js'); 14 | 15 | var ReportInfoModel = function(manifest) { 16 | this.reportName = manifest.reportName; 17 | this.reportUrl = manifest.getReportUrl(); 18 | this.time = new Date(manifest.time).toLocaleString(localeDetection(), { hour12: false }); 19 | 20 | this.parameters = _.concat( 21 | [{ name: 'time period', value: manifest.dateRange.split('_').join(' - ') }], 22 | _.map(manifest.parameters, function(value, key) { 23 | return { name: key, value: value }; 24 | }) 25 | ); 26 | }; 27 | 28 | module.exports = function() { 29 | var self = this; 30 | this.reportsInfo = ko.observable([]); 31 | 32 | asyncLoader.loadJSON('/allReports').then(function(reportsManifests) { 33 | self.reportsInfo(_.map(_.reverse(_.sortBy(reportsManifests, 'time')), function(manifest) { 34 | return new ReportInfoModel(_.mixin(manifest, ManifestMixin)); 35 | })); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /lib/web/models/series_group_model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | module.exports = function(groupDefinitions, opts) { 11 | var values = []; 12 | var options = opts || {}; 13 | 14 | this.name = _.map(groupDefinitions, 'name').join(' - '); 15 | this.groupDefinitions = groupDefinitions; 16 | 17 | this.addValue = function(obj) { 18 | values.push(obj); 19 | }; 20 | 21 | this.hasAnyKey = function(group, names) { 22 | var groupDefinition = _.find(groupDefinitions, { 'group': group }); 23 | return _.includes(names, groupDefinition.name); 24 | }; 25 | 26 | this.allValues = function() { 27 | if (options.sortBy) { 28 | return _.sortBy(values, options.sortBy); 29 | } 30 | return values; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /lib/web/models/template_register.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | metricRangeFilterTemplate: { id: 'metric-range-filters', file: 'metric_range_filters_control_template.html' }, 10 | percentageRangeFilterTemplate: { id: 'percentage-range-filters', file: 'percentage_range_filters_control_template.html' }, 11 | colorMapFilterTemplate: { id: 'color-map-filters', file: 'color_map_filters_control_template.html' }, 12 | groupListSelectionFilterTemplate: { id: 'grouplist-selection-filters', file: 'grouplist_selection_filters_control_template.html' }, 13 | textFilterTemplate: { id: 'text-filters', file: 'text_filters_control_template.html' }, 14 | colorMapWidgetTemplate: { id: 'color-map-widgets', file: 'color_map_widgets_control_template.html' }, 15 | itemListTooltipTemplate: { id: 'item-list-tooltip', file: 'item_list_tooltip_template.html' }, 16 | elementInfoTooltipTemplate: { id: 'element-info-tooltip', file: 'element_info_tooltip_template.html' } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/web/reports/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | 'hotspot-analysis': require('./hotspot_analysis.js'), 10 | 'complexity-trend': require('./complexity_trend.js'), 11 | 'sloc-trend': require('./sloc_trend.js'), 12 | 'sum-of-coupling': require('./sum_of_coupling.js'), 13 | 'temporal-coupling': require('./temporal_coupling.js'), 14 | 'system-evolution': require('./system_evolution.js'), 15 | 'commit-messages': require('./word_cloud.js'), 16 | 'developer-effort': require('./developer_effort.js'), 17 | 'developer-coupling': require('./developer_coupling.js'), 18 | 'knowledge-map': require('./knowledge_map.js') 19 | }; 20 | -------------------------------------------------------------------------------- /lib/web/reports/sum_of_coupling.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var d3 = require('d3'); 9 | 10 | var Model = require('../diagrams/bar_chart/vertical_bar_chart_model.js'), 11 | filters = require('../filters/index.js'); 12 | 13 | module.exports = function(manifest) { 14 | return { 15 | metadata: { 16 | title: 'Sum of coupling analysis', 17 | description: 'modules most frequently coupled with others in single commits', 18 | diagramSelectionTitle: 'Metric', 19 | dateRange: manifest.parseDateRange() 20 | }, 21 | graphModels: manifest.selectAvailableGraphs( 22 | [{ diagramName: 'sum-of-coupling' }] 23 | ).map(function(cfg) { 24 | return { 25 | id: cfg.diagramName, 26 | label: 'Sum of coupling', 27 | dataFile: manifest.dataFiles[0], 28 | controlTemplates: { 29 | filters: [{ name: 'textFilterTemplate', data: { labels: ['File path'] } }] 30 | }, 31 | diagram: { 32 | Model: Model, 33 | configuration: { 34 | style: { 35 | cssClass: 'bar-chart-diagram', 36 | width: 1000, 37 | height: 650, 38 | margin: { top: 30, right: 10, bottom: 0, left: 10 }, 39 | barWidth: 25, 40 | barColor: '#BFDDFB' 41 | }, 42 | series: { 43 | x: { scale: d3.scaleLinear(), labelProperty: 'path', valueProperty: 'path' }, 44 | y: { scale: d3.scaleLinear(), valueProperty: 'soc', labelProperty: '' } 45 | } 46 | }, 47 | controls: { 48 | filters: { 49 | pathFilter: { 50 | instance: new filters.RegExpValue(), 51 | group: 'text' 52 | } 53 | } 54 | } 55 | } 56 | }; 57 | }) 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /lib/web/reports/word_cloud.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var Model = require('../diagrams/word_cloud/diagram_model.js'), 11 | LayoutAdapter = require('../diagrams/word_cloud/cloud_layout_adapter.js'), 12 | ColorScaleFactory = require('../utils/color_scale_factory.js'), 13 | filters = require('../filters/index.js'); 14 | 15 | module.exports = function(manifest) { 16 | return { 17 | metadata: { 18 | title: 'Commit vocabulary', 19 | description: 'Word cloud of commit messages', 20 | diagramSelectionTitle: 'Time period', 21 | dateRange: manifest.parseDateRange() 22 | }, 23 | graphModels: manifest.selectAvailableGraphs( 24 | _.map(manifest.dataFiles, function(data) { 25 | var dates = data.timePeriod.split('_'); 26 | return { 27 | id: 'cwc-' + data.timePeriod, 28 | diagramName: 'commit-words', 29 | label: 'from ' + dates[0] + ' to ' + dates[1], 30 | dataFile: data, 31 | controlTemplates: { 32 | filters: [{ name: 'metricRangeFilterTemplate', data: { labels: ['Word occurency'] } }] 33 | }, 34 | diagram: { 35 | Model: Model, 36 | layoutAdapter: new LayoutAdapter({ width: 1000, height: 650, wordPadding: 10 }), 37 | configuration: { 38 | style: { 39 | cssClass: 'cloud-diagram', 40 | width: 1000, 41 | height: 650, 42 | minFontSize: 7 43 | }, 44 | colorScaleFactory: ColorScaleFactory.defaultOrdinal 45 | }, 46 | controls: { 47 | filters: { 48 | wordOccurenciesFilter: { 49 | instance: new filters.MetricRange(), 50 | group: 'metricRange', 51 | dataTransform: function(series) { return _.map(series, 'count'); } 52 | } 53 | } 54 | } 55 | } 56 | }; 57 | }) 58 | ) 59 | }; 60 | }; 61 | -------------------------------------------------------------------------------- /lib/web/utils/async_loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var Bluebird = require('bluebird'); 9 | 10 | var templateExists = function(id) { 11 | return window.document.getElementById(id) !== null; 12 | }; 13 | 14 | module.exports = { 15 | loadTemplate: function(file) { 16 | return window.fetch('/templates/' + file) 17 | .then(function(response) { return response.text(); }); 18 | }, 19 | loadTemplateIntoDocument: function(id, file) { 20 | if (templateExists(id)) { return Bluebird.resolve(); } 21 | 22 | return this.loadTemplate(file).then(function(content) { 23 | if (!templateExists(id)) { 24 | window.document.getElementById('templates').insertAdjacentHTML('beforeend', content); 25 | } 26 | }); 27 | }, 28 | loadJSON: function(requestUrl) { 29 | return window.fetch(requestUrl).then(function(response) { 30 | return response.json(); 31 | }); 32 | }, 33 | loadData: function(file) { 34 | return this.loadJSON('/data/' + file); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /lib/web/utils/color_scale_factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | d3 = require('d3'); 10 | 11 | var RotatingSequence = require('./rotating_sequence.js'); 12 | 13 | var DEFAULT_COLOR_GROUPS = _.map(d3.schemeCategory10, function(c) { 14 | var h = d3.hsl(c); 15 | return [d3.hsl(h.h, h.s, 0.30), d3.hsl(h.h, h.s, 0.80)]; 16 | }); 17 | 18 | module.exports = { 19 | defaultOrdinal: function(valuesRange, scheme) { 20 | var schemeCategory = scheme || d3.schemeCategory10; 21 | var scale = d3.scaleOrdinal(schemeCategory); 22 | if (valuesRange) { 23 | scale.domain(valuesRange); 24 | } 25 | return scale; 26 | }, 27 | gradientLinear: function(domain, colorsRange) { 28 | return d3.scaleLinear() 29 | .domain(domain) 30 | .range(colorsRange) 31 | .interpolate(d3.interpolateHsl); 32 | }, 33 | defaultGradientOrdinalGroups: function(domainGroups) { 34 | var colorsGroupSequence = new RotatingSequence(DEFAULT_COLOR_GROUPS); 35 | return _.mapValues(domainGroups, function(domainValues) { 36 | var colorScale = d3.scaleLinear() 37 | .domain([0, domainValues.length - 1]) 38 | .range(colorsGroupSequence.next()) 39 | .interpolate(d3.interpolateHsl); 40 | 41 | return function(value) { 42 | return colorScale(_.indexOf(domainValues, value)); 43 | }; 44 | }); 45 | }, 46 | sequentialRainbow: function(values) { 47 | var colorScale = d3.scaleSequential(d3.interpolateRainbow).domain([0, values.length + 1]); 48 | return function(value) { 49 | return colorScale(_.indexOf(values, value)); 50 | }; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /lib/web/utils/locale_detection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = function() { 9 | if (window.navigator.languages) { 10 | return window.navigator.languages[0]; 11 | } 12 | return window.navigator.language; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/web/utils/query_parameter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'); 9 | 10 | var QueryParameter = function() { this.values = []; }; 11 | 12 | QueryParameter.prototype.addValue = function(v) { this.values.push(decodeURIComponent(v)); }; 13 | QueryParameter.prototype.getValue = function() { return this.values[0]; }; 14 | QueryParameter.prototype.getValues = function() { return this.values; }; 15 | 16 | QueryParameter.fromRequestUrl = function() { 17 | var queryString = document.location.search.substr(1); 18 | return _.reduce(queryString.split('&'), function(urlParameters, paramString) { 19 | var nameValuePair = paramString.split('='); 20 | var paramName = nameValuePair[0]; 21 | if (_.isUndefined(urlParameters[paramName])) { 22 | urlParameters[paramName] = new QueryParameter(); 23 | } 24 | urlParameters[paramName].addValue(nameValuePair[1]); 25 | return urlParameters; 26 | }, {}); 27 | }; 28 | 29 | module.exports = QueryParameter; 30 | -------------------------------------------------------------------------------- /lib/web/utils/rotating_sequence.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = function(array) { 9 | var index = 0; 10 | 11 | this.next = function() { 12 | var nextElement = array[index++]; 13 | if (index === array.length) { 14 | index = 0; 15 | } 16 | return nextElement; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /lib/web/utils/scale_domain_factory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var _ = require('lodash'), 9 | d3 = require('d3'); 10 | 11 | var TYPE = { 12 | default: _.identity, 13 | zeroBased: function(dataArray) { 14 | return [0, d3.max(dataArray)]; 15 | }, 16 | extentBased: function(dataArray) { 17 | return d3.extent(dataArray); 18 | } 19 | }; 20 | 21 | module.exports = function(data, type) { 22 | if (_.isFunction(type)) { return type(data); } 23 | return TYPE[type || 'default'](data); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/web/view_model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'); 9 | 10 | var ReportModel = require('./models/report_model.js'), 11 | ReportsListModel = require('./models/reports_list_model.js'), 12 | ReportController = require('./controllers/report_controller.js'), 13 | asyncLoader = require('./utils/async_loader.js'); 14 | 15 | module.exports = function(parameters) { 16 | var self = this; 17 | 18 | this.reportModel = ko.observable(); 19 | this.reportsListModel = ko.observable(); 20 | 21 | if (parameters.reportId) { 22 | asyncLoader.loadData(parameters.reportId.getValue() + '/manifest.json').then(function(manifest) { 23 | var model = ReportModel.create(manifest); 24 | self.reportController = new ReportController(model.graphModels); 25 | self.reportModel(model); 26 | }); 27 | } else { 28 | this.reportsListModel = new ReportsListModel(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /lib/web/widgets/color_map.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | var ko = require('knockout'), 9 | _ = require('lodash'); 10 | 11 | var ColorScaleFactory = require('../utils/color_scale_factory.js'); 12 | var Widget = function() { 13 | this.hasData = ko.observable(false); 14 | }; 15 | 16 | Widget.prototype.init = function(values) { 17 | var colorScale = _.isEmpty(values) ? ColorScaleFactory.defaultOrdinal() : ColorScaleFactory.sequentialRainbow(values); 18 | 19 | this.hasData(values.length > 0); 20 | 21 | this.colorMap = _.map(values, function(name) { 22 | return { name: name, color: colorScale(name) }; 23 | }); 24 | }; 25 | 26 | module.exports = Widget; 27 | -------------------------------------------------------------------------------- /lib/web/widgets/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | module.exports = { 9 | ColorMap: require('./color_map.js') 10 | }; 11 | -------------------------------------------------------------------------------- /public/styles/d3_tip.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | @tooltip-arrow: { 9 | display: inline; 10 | color: rgba(0, 0, 0, 0.8); 11 | position: absolute; 12 | text-align: center; 13 | }; 14 | 15 | .d3-tip { 16 | z-index: 10; 17 | line-height: 1; 18 | font-size: 0.75em; 19 | border-radius: 5px; 20 | box-shadow: 4px 4px 12px rgba(0,0,0,.5); 21 | transition: opacity 300ms linear; 22 | 23 | &.top-arrow { 24 | &:after { 25 | @tooltip-arrow(); 26 | top: -10px; 27 | left: 45%; 28 | content: "\25B2"; 29 | line-height: 1; 30 | } 31 | } 32 | &.right-arrow { 33 | &:after { 34 | @tooltip-arrow(); 35 | right: -10px; 36 | top: 50%; 37 | content: "\25BA"; 38 | line-height: 0.5; 39 | } 40 | } 41 | &.bottom-arrow { 42 | &:after { 43 | @tooltip-arrow(); 44 | bottom: -12px; 45 | left: 45%; 46 | content: "\25BC"; 47 | line-height: 1; 48 | } 49 | } 50 | &.left-arrow { 51 | &:after { 52 | @tooltip-arrow(); 53 | left: -10px; 54 | top: 50%; 55 | content: "\25C0"; 56 | line-height: 0.5; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /public/styles/diagrams.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | @import 'diagrams/circle_packing_diagram.less'; 9 | @import 'diagrams/line_chart_diagram.less'; 10 | @import 'diagrams/bar_chart_diagram.less'; 11 | @import 'diagrams/word_cloud_diagram.less'; 12 | @import 'diagrams/treemap_diagram.less'; 13 | @import 'diagrams/network_graph_diagram.less'; 14 | 15 | .graph-container { 16 | .diagram-wrapper { 17 | width: 100%; 18 | height: 100%; 19 | 20 | .diagram { 21 | resize: both; 22 | overflow: auto; 23 | padding: 5px; 24 | border: solid 1px #A0A0A0; 25 | } 26 | } 27 | .msg-large { 28 | color: #DD1010; 29 | text-align: center; 30 | font-size: 1.4em; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/styles/diagrams/bar_chart_diagram.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .bar-chart-diagram { 9 | .chart-legend-mixin(); 10 | .chart-axis-mixin(); 11 | 12 | background-color: #F3F9FF; 13 | 14 | &#sum-of-coupling { 15 | padding: 0; 16 | } 17 | 18 | .bars { 19 | text { 20 | font-size: 14px; 21 | } 22 | } 23 | } 24 | 25 | .d3-tip { 26 | &.bar-chart-diagram { 27 | background-color: rgba(0, 0, 0, 0.8); 28 | 29 | .element-property-tooltip-mixin(@value-color: #FFFF80); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/styles/diagrams/line_chart_diagram.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .line-mixin() { 9 | .line { 10 | fill: none; 11 | stroke-width: 2px; 12 | } 13 | } 14 | 15 | .line-chart-diagram { 16 | background-color: #FAFAFA; 17 | 18 | &#system-evolution-revisions, &#system-evolution-commits, &#system-evolution-authors, &#system-evolution-churn { 19 | .chart-axis-mixin(); 20 | .line-mixin(); 21 | .chart-legend-mixin(); 22 | } 23 | 24 | &#lc-total, &#lc-func-mean, &#lc-func-sd, &#lc-sloc { 25 | .chart-axis-mixin(); 26 | .line-mixin(); 27 | .chart-legend-mixin(); 28 | 29 | .brush rect { 30 | fill-opacity: 0.1; 31 | } 32 | 33 | .zoom { 34 | cursor: move; 35 | fill: none; 36 | pointer-events: all; 37 | } 38 | 39 | .dots { 40 | opacity: 0.8 41 | } 42 | } 43 | } 44 | 45 | .d3-tip { 46 | &.line-chart-diagram { 47 | .element-info-tooltip-mixin( 48 | @title-color: #4682B4; 49 | @title-bg-color: #FFFFFF; 50 | @font-weight: bold 51 | ); 52 | 53 | .property-list { 54 | .element-property-tooltip-mixin( 55 | @value-color: #000000; 56 | @value-text-align: left; 57 | @label-font-weight: bold; 58 | @grid-template-columns: 1fr 2fr; 59 | ); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/styles/diagrams/network_graph_diagram.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .network-graph-diagram { 9 | .links line { 10 | stroke: #AAAAAA; 11 | stroke-opacity: 0.6; 12 | pointer-events: none; 13 | 14 | &.highlighted { 15 | stroke: #000000; 16 | stroke-opacity: 1; 17 | pointer-events: visible; 18 | } 19 | 20 | &.link-hidden { 21 | stroke-opacity: 0.1; 22 | } 23 | 24 | &:hover { 25 | stroke: #4682B4; 26 | stroke-width: 5px; 27 | } 28 | } 29 | 30 | .nodes circle { 31 | stroke: #FFFFFF; 32 | stroke-width: 1.5px; 33 | 34 | &.node-hidden { 35 | fill-opacity: 0.1; 36 | } 37 | } 38 | } 39 | 40 | .d3-tip { 41 | &.network-graph-diagram { 42 | &.node-info { 43 | .item-list { 44 | background-color: #EEEEEE; 45 | border: 1px solid #DDDDDD; 46 | border-radius: 5px; 47 | padding: 5px; 48 | .item { 49 | text-align: center; 50 | color: #000000; 51 | padding: 2px 0; 52 | &.title { 53 | font-weight: bold; 54 | } 55 | } 56 | } 57 | } 58 | 59 | &.link-info { 60 | .element-info-tooltip-mixin(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /public/styles/diagrams/treemap_diagram.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .treemap-diagram { 9 | padding: 5px; 10 | background: #333333; 11 | 12 | .root-tile { 13 | fill: #333333; 14 | rect { 15 | stroke: none; 16 | } 17 | 18 | text { 19 | font-size: 16px; 20 | fill: #FFFFFF; 21 | pointer-events: visible; 22 | cursor: pointer; 23 | } 24 | } 25 | 26 | .treemap-container { 27 | shape-rendering: crispEdges; 28 | 29 | rect { 30 | pointer-events: visibleFill; 31 | fill: none; 32 | stroke: #000000; 33 | stroke-width: 1px; 34 | 35 | &.parent-tile { 36 | fill: #F3F9FF; 37 | fill-opacity: 0.3; 38 | 39 | &:hover { 40 | cursor: auto; 41 | stroke-width: 3px; 42 | stroke: #FFFFFF; 43 | fill-opacity: 0.45; 44 | } 45 | 46 | &.last:hover { 47 | fill-opacity: 0; 48 | } 49 | } 50 | } 51 | 52 | text.tile-title { 53 | pointer-events: none; 54 | tspan.parent-tile { 55 | fill: #0044FF; 56 | font-weight: bold; 57 | font-size: 14px; 58 | text-shadow: 0 1px 0 #FFFFFF, 1px 0 0 #FFFFFF, -1px 0 0 #FFFFFF, 0 -1px 0 #FFFFFF; 59 | } 60 | 61 | tspan.child-tile, tspan.leaf-tile { 62 | font-size: 12px; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/styles/diagrams/word_cloud_diagram.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .cloud-diagram { 9 | .word-cloud { 10 | text { 11 | text-anchor: middle; 12 | font-family: 'Impact'; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /public/styles/graph_controls.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .report-controls { 9 | .control-name { 10 | padding: 0 5px; 11 | color: #4682B4; 12 | font-size: 1.3em; 13 | font-weight: bold; 14 | margin-bottom: 3px; 15 | } 16 | 17 | .metric-filter { 18 | output { 19 | float: right; 20 | color: #000000; 21 | padding-right: 5px; 22 | font-weight: normal; 23 | text-align: right; 24 | } 25 | } 26 | 27 | .filter-parameter { 28 | input[type=text] { 29 | width: 190px; 30 | margin: 5px 0px 0px 5px; 31 | } 32 | } 33 | 34 | .value-map { 35 | .control-values-mixin(); 36 | 37 | &.bordered { 38 | border: 1px solid; 39 | border-radius: 5px; 40 | } 41 | 42 | .filter-selectable { 43 | font-size: 1.2em; 44 | opacity: 0.3; 45 | padding: 3px 2px; 46 | 47 | &:hover { 48 | cursor: default; 49 | background-color: #DDDDDD; 50 | } 51 | 52 | &.highlighted { 53 | opacity: 1; 54 | } 55 | } 56 | } 57 | 58 | .color-map { 59 | .control-values-mixin(); 60 | 61 | &.bordered { 62 | border: 1px solid; 63 | border-radius: 5px; 64 | } 65 | 66 | .color-box { 67 | margin: 5px 0 0 5px; 68 | padding: 5px; 69 | border: 1px solid #DDDDDD; 70 | display: inline-block; 71 | } 72 | 73 | .color-value { 74 | font-size: 1.2em; 75 | color: #000000; 76 | padding-left: 5px; 77 | bottom: 2px; 78 | position: relative; 79 | } 80 | 81 | .filter-selectable { 82 | &:hover { 83 | cursor: default; 84 | background-color: #DDDDDD; 85 | } 86 | 87 | &.opaque { 88 | background-color: transparent; 89 | opacity: 0.1; 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /public/styles/slider.less: -------------------------------------------------------------------------------- 1 | /* 2 | * code-forensics 3 | * Copyright (C) 2016-2021 Silvio Montanari 4 | * Distributed under the GNU General Public License v3.0 5 | * see http://www.gnu.org/licenses/gpl.html 6 | */ 7 | 8 | .slider-thumb { 9 | -webkit-appearance: none; 10 | -moz-appearance: none; 11 | appearance: none; 12 | width: 10px; 13 | height: 10px; 14 | cursor: pointer; 15 | border-radius: 50%; 16 | background-color: #4682B4; 17 | } 18 | 19 | .slider { 20 | outline: none; 21 | width: 90%; 22 | 23 | &::-webkit-slider-thumb { 24 | .slider-thumb(); 25 | } 26 | 27 | &::-moz-range-thumb { 28 | .slider-thumb(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/templates/color_map_filters_control_template.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /public/templates/color_map_widgets_control_template.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /public/templates/element_info_tooltip_template.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /public/templates/grouplist_selection_filters_control_template.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /public/templates/item_list_tooltip_template.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /public/templates/metric_range_filters_control_template.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /public/templates/percentage_range_filters_control_template.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /public/templates/text_filters_control_template.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /scripts/check-license-header.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require('fs'), 4 | Bluebird = require('bluebird'), 5 | readline = require('readline'), 6 | ansi = require('ansi-colors'), 7 | glob = require('glob'); 8 | 9 | var FileData = function(pathname) { 10 | var CHECK_REGEXP = /Copyright \(C\) \d+-\d+ Silvio Montanari/; 11 | var lineCounter = 0; 12 | 13 | this.scan = function() { 14 | var inputStream = fs.createReadStream(pathname); 15 | var rl = readline.createInterface({ input: inputStream }); 16 | return new Bluebird(function(resolve) { 17 | rl.on('line', function(line) { 18 | lineCounter++; 19 | if (lineCounter === 3) { 20 | resolve({ path: pathname, check: CHECK_REGEXP.test(line) }); 21 | rl.close(); 22 | } 23 | }); 24 | }); 25 | }; 26 | }; 27 | 28 | Bluebird.map(glob.sync('{./lib/**/*.js,./public/styles/**/*.less}'), function(filePath) { 29 | return new FileData(filePath).scan(); 30 | }).then(function(results) { 31 | var invalidFiles = results.filter(function(r) { return r.check === false; }); 32 | /*eslint-disable no-console, no-process-exit*/ 33 | if (invalidFiles.length > 0) { 34 | console.log('Missing header licensing in files: '); 35 | invalidFiles.forEach(function(file) { 36 | console.log(ansi.red(file.path)); 37 | }); 38 | process.exit(1); 39 | } 40 | /*eslint-disable no-console, no-process-exit*/ 41 | }); 42 | -------------------------------------------------------------------------------- /scripts/check-node-version.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var _ = require('lodash'); 4 | var path = require('path'); 5 | var semver = require('semver'); 6 | var packageConfig = require(path.join(__dirname,'..', 'package.json')); 7 | 8 | function verifyTargetVersion(target) { 9 | _.each(packageConfig.dependencies, function(depVersion, depName) { 10 | var package = require(path.join(__dirname, '..', 'node_modules', depName, 'package.json')); 11 | var requiredVersion = _.property('engines.node')(package); 12 | if (requiredVersion && requiredVersion !== '*') { 13 | if (!semver.satisfies(target, requiredVersion)) { 14 | console.log(package.name + ': ' + requiredVersion); 15 | } 16 | } 17 | }); 18 | } 19 | 20 | var args = process.argv.slice(2); 21 | if (args.length === 0) { 22 | console.error('Please provide a target NodeJS version to check against'); 23 | process.exit(1); 24 | } 25 | 26 | verifyTargetVersion(semver.coerce(args[0])); 27 | -------------------------------------------------------------------------------- /spec/analysers/code_maat/docker_command_definition.spec.js: -------------------------------------------------------------------------------- 1 | var commandDefinition = require('analysers/code_maat/docker_command_definition'), 2 | command = require('command'); 3 | 4 | var appConfig = require('runtime/app_config'); 5 | var helpers = require('../../jest_helpers'); 6 | 7 | describe.each([ 8 | [{ image: 'test-codemaat-image', volume: '/test/volume/dir' }], 9 | [{ image: 'test-codemaat-image' }] 10 | ])('codemaat docker command definition', function(config) { 11 | var subject, mockCheck; 12 | beforeEach(function() { 13 | mockCheck = { 14 | verifyExecutable: jest.fn(), 15 | verifyConfigurationProperty: jest.fn() 16 | }; 17 | process.cwd = jest.fn().mockReturnValue('test/current/dir'); 18 | helpers.appConfigStub({ 19 | basedir: '/test/project/dir', 20 | codeMaat: { docker: config } 21 | }); 22 | commandDefinition(); 23 | subject = command.Command.definitions.getDefinition('codemaat-docker'); 24 | }); 25 | 26 | it('defines the "codemaat-docker" command', function() { 27 | var expectedHostVolume = config.volume || 'test/current/dir'; 28 | expect(subject).toEqual({ 29 | cmd: 'docker', 30 | args: [ 31 | 'run', 32 | '--rm', 33 | '-v', 34 | expectedHostVolume + ':/data', 35 | 'test-codemaat-image' 36 | ], 37 | installCheck: expect.any(Function), 38 | config: { 39 | containerVolume: '/data', 40 | hostVolume: expectedHostVolume 41 | } 42 | }); 43 | }); 44 | 45 | it('checks the docker executable', function() { 46 | subject.installCheck.apply(mockCheck); 47 | 48 | expect(mockCheck.verifyExecutable).toHaveBeenCalledWith('docker', expect.any(String)); 49 | }); 50 | 51 | it('checks the docker image configuration property', function() { 52 | subject.installCheck.apply(mockCheck); 53 | 54 | expect(mockCheck.verifyConfigurationProperty).toHaveBeenCalledWith(appConfig, 'codeMaat.docker.image', expect.any(String)); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /spec/analysers/code_maat/java_command_definition.spec.js: -------------------------------------------------------------------------------- 1 | var commandDefinition = require('analysers/code_maat/java_command_definition'), 2 | command = require('command'); 3 | 4 | var helpers = require('../../jest_helpers'); 5 | 6 | describe('codemaat java command definition', function() { 7 | var subject, mockCheck; 8 | beforeEach(function() { 9 | mockCheck = { 10 | verifyExecutable: jest.fn(), 11 | verifyPackage: jest.fn(), 12 | verifyFile: jest.fn() 13 | }; 14 | }); 15 | 16 | describe('with default configuration', function() { 17 | beforeEach(function() { 18 | commandDefinition(); 19 | subject = command.Command.definitions.getDefinition('codemaat'); 20 | }); 21 | 22 | it('defines the "codemaat" command', function() { 23 | expect(subject).toEqual({ 24 | cmd: 'java', 25 | args: [ 26 | '-Djava.awt.headless=true', 27 | { '-jar': expect.stringMatching('code-maat-1.0.1-standalone.jar') } 28 | ], 29 | installCheck: expect.any(Function) 30 | }); 31 | }); 32 | }); 33 | 34 | describe('with custom configuration', function() { 35 | beforeEach(function() { 36 | helpers.appConfigStub({ codeMaat: { packageFile: '/some/test/codemaat-package.jar' } }); 37 | commandDefinition(); 38 | subject = command.Command.definitions.getDefinition('codemaat'); 39 | }); 40 | 41 | afterEach(function() { 42 | helpers.appConfigRestore(); 43 | }); 44 | 45 | it('defines the "codemaat" command', function() { 46 | expect(subject).toEqual({ 47 | cmd: 'java', 48 | args: [ 49 | '-Djava.awt.headless=true', 50 | { '-jar': '/some/test/codemaat-package.jar' } 51 | ], 52 | installCheck: expect.any(Function) 53 | }); 54 | }); 55 | }); 56 | 57 | it('checks the java executable', function() { 58 | subject.installCheck.apply(mockCheck); 59 | 60 | expect(mockCheck.verifyExecutable).toHaveBeenCalledWith('java', expect.any(String)); 61 | expect(mockCheck.verifyFile).toHaveBeenCalledWith('/some/test/codemaat-package.jar', expect.any(String)); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /spec/analysers/flog/flog_parser.spec.js: -------------------------------------------------------------------------------- 1 | var FlogParser = require('analysers/flog/flog_parser'); 2 | 3 | describe('FlogParser', function() { 4 | var content = ' 643.7: flog total\n' + 5 | ' 8.7: flog/method average\n' + 6 | '\n' + 7 | ' 46.7: TestModuleName#method1\n' + 8 | ' 41.4: TestModuleName#method2\n'; 9 | 10 | it('returns the complexity stats for the module', function() { 11 | var output = new FlogParser().read(content); 12 | 13 | expect(output).toEqual({ 14 | totalComplexity: 643.7, 15 | averageComplexity: 8.7, 16 | methodComplexity: [ 17 | { name: 'TestModuleName#method1', complexity: 46.7 }, 18 | { name: 'TestModuleName#method2', complexity: 41.4 } 19 | ] 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /spec/analysers/word_count/text_filters.spec.js: -------------------------------------------------------------------------------- 1 | var textFilters = require('analysers/word_count/text_filters'); 2 | 3 | describe('TextFilters', function() { 4 | var rejectFn; 5 | describe('.createRejectFn()', function() { 6 | beforeEach(function() { 7 | rejectFn = new textFilters.createRejectFn([ 8 | 'foo', 9 | /bar/, 10 | function(text) { return text === 'baz'; } 11 | ]); 12 | }); 13 | 14 | it("accepts words that don't match any expression", function() { 15 | expect(rejectFn('quz')).toBe(false); 16 | }); 17 | 18 | it('rejects words matching a string blacklist expression', function() { 19 | expect(rejectFn('foo')).toBe(true); 20 | }); 21 | 22 | it('rejects words matching a regexp blacklist expression', function() { 23 | expect(rejectFn('qwebarzxc')).toBe(true); 24 | }); 25 | 26 | it('rejects words matching a function blacklist expression', function() { 27 | expect(rejectFn('baz')).toBe(true); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /spec/analysers/word_count/word_count_analyser.spec.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream'), 2 | Bluebird = require('bluebird'); 3 | 4 | var WordCountAnalyser = require('analysers/word_count/word_count_analyser'); 5 | 6 | describe('WordCountAnalyser', function() { 7 | var inputStream; 8 | beforeEach(function() { 9 | inputStream = new stream.PassThrough(); 10 | }); 11 | 12 | describe('with no filters', function() { 13 | it('returns all the input words', function() { 14 | return new Bluebird(function(done) { 15 | inputStream.pipe(new WordCountAnalyser().textAnalysisStream()) 16 | .on('data', function(wordCountReport) { 17 | expect(wordCountReport).toEqual([ 18 | { text: 'message', count: 2 }, 19 | { text: 'first', count: 1 }, 20 | { text: 'second', count: 1 } 21 | ]); 22 | }) 23 | .on('end', done); 24 | 25 | inputStream.write('First message\n'); 26 | inputStream.write('Second message\n'); 27 | inputStream.end(); 28 | }); 29 | }); 30 | }); 31 | 32 | describe('with text filters', function() { 33 | it('returns the filtered words', function() { 34 | return new Bluebird(function(done) { 35 | inputStream.pipe(new WordCountAnalyser().textAnalysisStream([ 36 | /^\d+ message/, 37 | 'foo', 38 | function(w) { return w === 'qaz'; } 39 | ])) 40 | .on('data', function(wordCountReport) { 41 | expect(wordCountReport).toEqual([ 42 | { text: 'message', count: 3 }, 43 | { text: 'bar', count: 2 }, 44 | { text: 'baz-qaz', count: 1 } 45 | ]); 46 | }) 47 | .on('end', done); 48 | 49 | inputStream.write('123 message\n'); 50 | inputStream.write('Bar message\n'); 51 | inputStream.write('Foo Bar message\n'); 52 | inputStream.write('Baz-qaz message\n'); 53 | inputStream.write('qaz\n'); 54 | inputStream.end(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /spec/analysers/word_count/word_counter.spec.js: -------------------------------------------------------------------------------- 1 | var WordCounter = require('analysers/word_count/word_counter'); 2 | 3 | describe('WordCounter', function() { 4 | it('returns a report of all words count ordered by the highest value', function() { 5 | var counter = new WordCounter(); 6 | counter.addWords(['foo', 'bar', 'baz']); 7 | counter.addWords(['bar', 'qaz', 'qux']); 8 | counter.addWords(['qaz', 'bar', 'foo']); 9 | 10 | expect(counter.report()).toEqual([ 11 | { text: 'bar', count: 3 }, 12 | { text: 'foo', count: 2 }, 13 | { text: 'qaz', count: 2 }, 14 | { text: 'baz', count: 1 }, 15 | { text: 'qux', count: 1 } 16 | ]); 17 | }); 18 | 19 | it('handles special javascript keywords', function() { 20 | var counter = new WordCounter(); 21 | counter.addWords(['foo', 'bar', 'constructor']); 22 | counter.addWords(['bar', 'toString', 'qux']); 23 | counter.addWords(['toString', 'bar', 'foo']); 24 | 25 | expect(counter.report()).toEqual([ 26 | { text: 'bar', count: 3 }, 27 | { text: 'foo', count: 2 }, 28 | { text: 'toString', count: 2 }, 29 | { text: 'constructor', count: 1 }, 30 | { text: 'qux', count: 1 } 31 | ]); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/api_middleware/load_reports.spec.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | Path = require('path'), 3 | Bluebird = require('bluebird'), 4 | fs = require('fs'), 5 | del = require('del'), 6 | mkdirp = require('mkdirp'); 7 | 8 | var logger = require('log'); 9 | var loadReports = require('api_middleware/load_reports'); 10 | 11 | describe('loadReports', function() { 12 | // eslint-disable-next-line no-undef 13 | var outputDir = Path.join(TEST_FIXTURES_DIR, 'test_reports'); 14 | 15 | var createReportManifests = function() { 16 | _.each([ 17 | '{ "name": "A report manifest" }', 18 | undefined, 19 | '{ "name": "Another report manifest" }' 20 | ], function(json, index) { 21 | var reportDir = Path.join(outputDir, 'report-' + (index + 1)); 22 | mkdirp.sync(reportDir); 23 | if (json) { 24 | fs.writeFileSync(Path.join(reportDir, 'manifest.json'), json); 25 | } 26 | }); 27 | }; 28 | 29 | beforeEach(function() { 30 | createReportManifests(); 31 | }); 32 | 33 | afterEach(function() { 34 | return del(outputDir); 35 | }); 36 | 37 | it('returns a list of streams for each valid report manifest', function() { 38 | expect(loadReports(outputDir)).toHaveLength(2); 39 | }); 40 | 41 | it.each([ 42 | [0, { name: 'A report manifest' }], 43 | [1, { name: 'Another report manifest' }] 44 | ])('returns a stream with the manifest data', function(reportIndex, expectedData) { 45 | var streams = loadReports(outputDir); 46 | return new Bluebird(function(done) { 47 | streams[reportIndex] 48 | .on('data', function(data) { 49 | expect(data).toEqual(expectedData); 50 | }) 51 | .on('end', done); 52 | }); 53 | }); 54 | 55 | it('logs', function() { 56 | loadReports(outputDir); 57 | 58 | expect(logger.error).toHaveBeenCalledWith( 59 | expect.stringMatching(/Report manifest not found: .*\/test_reports\/report-2\/manifest.json/) 60 | ); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /spec/graph_support/tree.spec.js: -------------------------------------------------------------------------------- 1 | var Tree = require('graph_support/tree'); 2 | 3 | describe('Tree', function() { 4 | var subject; 5 | beforeEach(function() { 6 | subject = new Tree('/root/path', 'path'); 7 | }); 8 | 9 | describe('.addNode()', function() { 10 | var childNode; 11 | 12 | beforeEach(function() { 13 | childNode = subject.addNode({ 14 | path: '/root/path/branch/child', 15 | stringProperty: 'test-property', 16 | numberProperty: 123 17 | }); 18 | }); 19 | 20 | it('has a root node with empty name', function() { 21 | expect(subject.rootNode.name).toBeUndefined(); 22 | }); 23 | 24 | it('returns the child node', function() { 25 | expect(childNode.name).toEqual('child'); 26 | expect(childNode.children).toEqual([]); 27 | expect(childNode.stringProperty).toEqual('test-property'); 28 | expect(childNode.numberProperty).toEqual(123); 29 | }); 30 | 31 | it('generates the children tree structure', function() { 32 | expect(subject.rootNode.children).toHaveLength(1); 33 | expect(subject.rootNode.children[0].name).toEqual('branch'); 34 | expect(subject.rootNode.children[0].children).toEqual([childNode]); 35 | }); 36 | 37 | it('adds another child node', function() { 38 | var node = subject.addNode({ 39 | path: '/root/path/branch/child/anotherChild', 40 | booleanProperty: true, 41 | numberProperty: 456 42 | }); 43 | 44 | expect(childNode.children).toEqual([node]); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/graph_support/tree_node.spec.js: -------------------------------------------------------------------------------- 1 | var TreeNode = require('graph_support/tree_node'); 2 | 3 | describe('TreeNode', function() { 4 | var subject; 5 | beforeEach(function() { 6 | subject = new TreeNode('node/path'); 7 | }); 8 | 9 | it('creates a child tree when it does not exist', function() { 10 | var node = subject.getChildNode('some/child'); 11 | 12 | expect(node.name).toEqual('child'); 13 | expect(node.children).toEqual([]); 14 | expect(subject.children[0].name).toEqual('some'); 15 | expect(subject.children[0].children).toEqual([node]); 16 | }); 17 | 18 | it('supports children with the same name as any of the indirect parents', function() { 19 | var parent = subject.getChildNode('parent/node'); 20 | var child = subject.getChildNode('parent/node/parent'); 21 | 22 | expect(child).not.toBe(parent); 23 | }); 24 | 25 | it('supports children with the same name as the direct parent node', function() { 26 | var parent = subject.getChildNode('parent'); 27 | var child = subject.getChildNode('parent/parent'); 28 | 29 | expect(child).not.toBe(parent); 30 | }); 31 | 32 | it('returns an existing child node', function() { 33 | var node1 = subject.getChildNode('some/node/child'); 34 | var node2 = subject.getChildNode('some').getChildNode('node/child'); 35 | 36 | expect(node1).toBe(node2); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /spec/graph_support/weighted_tree.spec.js: -------------------------------------------------------------------------------- 1 | var WeightedTree = require('graph_support/weighted_tree'); 2 | 3 | describe('WeightedTree', function() { 4 | var subject; 5 | it('returns a one node tree with no weight', function() { 6 | var node = new WeightedTree('test/root', 'branch', {weightedProperty: 'value'}).rootNode(); 7 | 8 | expect(node.name).toBeUndefined(); 9 | expect(node.children).toEqual([]); 10 | }); 11 | 12 | describe('for a normalised weight tree', function() { 13 | beforeEach(function() { 14 | subject = new WeightedTree('test/root', 'branch', {weightedProperty: 'value', normalised: true}); 15 | }); 16 | 17 | it('returns the root of a tree with the relative weight value', function() { 18 | [ 19 | { branch: 'test/root/b1/item1', value: 2 }, 20 | { branch: 'test/root/b1/item2', value: 5 }, 21 | { branch: 'test/root/b2/item3', value: 7 } 22 | ].forEach(subject.withItem.bind(subject)); 23 | 24 | var node = subject.rootNode(); 25 | 26 | expect(node.getChildNode('b1/item1').weight).toBeCloseTo(0.286, 3); 27 | expect(node.getChildNode('b1/item2').weight).toBeCloseTo(0.714, 3); 28 | expect(node.getChildNode('b2/item3').weight).toBeCloseTo(1.0, 3); 29 | }); 30 | }); 31 | 32 | describe('for non normalised weight tree', function() { 33 | beforeEach(function() { 34 | subject = new WeightedTree('test/root', 'branch', {weightedProperty: 'value', weightPropertyName: 'load', normalised: false}); 35 | }); 36 | 37 | it('returns the root of a tree with the absolute weight value', function() { 38 | [ 39 | { branch: 'test/root/b1/item1', value: 3 }, 40 | { branch: 'test/root/b1/item2', value: 6 }, 41 | { branch: 'test/root/b2/item3', value: 8 } 42 | ].forEach(subject.withItem.bind(subject)); 43 | 44 | var node = subject.rootNode(); 45 | 46 | expect(node.getChildNode('b1/item1').load).toEqual(3); 47 | expect(node.getChildNode('b1/item2').load).toEqual(6); 48 | expect(node.getChildNode('b2/item3').load).toEqual(8); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /spec/jest_env_setup.js: -------------------------------------------------------------------------------- 1 | var mtz = require('moment-timezone'); 2 | 3 | jest.mock('gulp'); 4 | jest.mock('log'); 5 | 6 | beforeAll(function() { 7 | mtz.tz.setDefault('UTC'); 8 | }); 9 | 10 | afterAll(function() { 11 | mtz.tz.setDefault(); 12 | }); 13 | -------------------------------------------------------------------------------- /spec/jest_helpers.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var appConfig = require('runtime/app_config'); 3 | 4 | module.exports = { 5 | appConfigStub: function(config) { 6 | jest.spyOn(appConfig, 'get').mockImplementation(function(property) { 7 | return _.get(config, property); 8 | }); 9 | }, 10 | appConfigRestore: function() { 11 | appConfig.get.mockRestore(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /spec/log/logger.spec.js: -------------------------------------------------------------------------------- 1 | var FakeTimers = require('@sinonjs/fake-timers'); 2 | 3 | var logger = require('log'); 4 | jest.unmock('log'); 5 | 6 | describe('Logger', function() { 7 | var clock; 8 | var assertStdout = function(msg) { 9 | expect(process.stdout.write).toHaveBeenCalledWith('[10:14:57] '); 10 | expect(console.log).toHaveBeenCalledWith(expect.stringMatching(msg)); 11 | }; 12 | 13 | beforeEach(function() { 14 | clock = FakeTimers.install({ now: new Date('2015-10-22T10:14:57.000Z') }); 15 | process.stdout.write = jest.fn(); 16 | console.log = jest.fn(); 17 | }); 18 | 19 | afterEach(function() { 20 | clock.uninstall(); 21 | }); 22 | 23 | it.each([ 24 | ['debug'], ['info'] 25 | ])('Writes a log message with detail', function(logLevel) { 26 | logger[logLevel]('test message: ', 'test detail'); 27 | assertStdout(/test message: .+test detail/); 28 | }); 29 | 30 | it.each([ 31 | ['log'], ['warn'], ['error'] 32 | ])('Writes a log message', function(logLevel) { 33 | logger[logLevel]('test message'); 34 | assertStdout(/test message/); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /spec/models/task/runners/default_runner.spec.js: -------------------------------------------------------------------------------- 1 | var DefaultRunner = require('models/task/runners/default_runner'); 2 | 3 | describe('DefaultRunner', function() { 4 | var doneCallback; 5 | beforeEach(function() { 6 | doneCallback = jest.fn().mockName('done'); 7 | }); 8 | 9 | describe('when a task function is defined', function() { 10 | it('executes the task function passing through the done callback', function() { 11 | var task = { 12 | run: jest.fn().mockName('taskFunction') 13 | }; 14 | new DefaultRunner(task).run(doneCallback); 15 | 16 | expect(task.run).toHaveBeenCalledWith(doneCallback); 17 | }); 18 | 19 | it('returns the output of the task function', function() { 20 | var output = new DefaultRunner({ 21 | run: function() { return 123; } 22 | }).run(doneCallback); 23 | 24 | expect(output).toEqual(123); 25 | }); 26 | }); 27 | 28 | describe('when a task function is not defined', function() { 29 | it('returns undefined and executes the callback', function() { 30 | var output = new DefaultRunner({}).run(doneCallback); 31 | 32 | expect(output).toBeUndefined(); 33 | expect(doneCallback).toHaveBeenCalled(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /spec/models/time_interval/time_period.spec.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | 3 | var TimePeriod = require('models/time_interval/time_period'); 4 | 5 | describe('TimePeriod', function() { 6 | var period; 7 | beforeEach(function() { 8 | period = { 9 | start: moment('2013-02-08 09:30:26.123'), 10 | end: moment('2014-01-18 19:10:46.456') 11 | }; 12 | }); 13 | 14 | it('concatenates start and end date', function() { 15 | expect(new TimePeriod(period, 'YYYY-MM-DD').toString()).toEqual('2013-02-08_2014-01-18'); 16 | }); 17 | 18 | it('returns an object with ISO formatted dates', function() { 19 | expect(new TimePeriod(period, 'YYYY-MM-DD').toISOFormat()).toEqual({ 20 | startDate: '2013-02-08T09:30:26.123Z', 21 | endDate: '2014-01-18T19:10:46.456Z' 22 | }); 23 | }); 24 | 25 | it('returns the dates displayed with the given format', function() { 26 | expect(new TimePeriod(period, 'MM-DD-YYYY').toDisplayFormat()).toEqual({ 27 | startDate: '02-08-2013', 28 | endDate: '01-18-2014' 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/reporting/merge_strategies.spec.js: -------------------------------------------------------------------------------- 1 | var MergeStrategies = require('reporting/merge_strategies'); 2 | 3 | describe('MergeStrategies', function() { 4 | var reportItem, dataSourceItem; 5 | beforeEach(function() { 6 | reportItem = { a: 123, b: 456 }; 7 | dataSourceItem = { c: 'qwe', d: 'asd' }; 8 | }); 9 | 10 | describe('extension', function() { 11 | it('extends the report item by transforming the data source item', function() { 12 | MergeStrategies.extension('d')(reportItem, dataSourceItem); 13 | 14 | expect(reportItem).toEqual({ a: 123, b: 456, d: 'asd' }); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /spec/reporting/object_transformer.spec.js: -------------------------------------------------------------------------------- 1 | var objectTransformer = require('reporting/object_transformer'); 2 | 3 | describe('objectTransformer', function() { 4 | var obj; 5 | beforeEach(function() { 6 | obj = { a: 123, b: 456, c: 789 }; 7 | }); 8 | 9 | it('extracts a property by name', function() { 10 | expect(objectTransformer(obj, 'a')).toEqual({ a: 123 }); 11 | }); 12 | 13 | it('extracts a list of properties by name', function() { 14 | expect(objectTransformer(obj, ['a', 'c'])).toEqual({ a: 123, 'c': 789 }); 15 | }); 16 | 17 | it('transforms the object through a function', function() { 18 | expect(objectTransformer(obj, function(o) { return {sum: o.a + o.b}; })).toEqual({ sum: 579 }); 19 | }); 20 | 21 | it('returns the object itself', function() { 22 | expect(objectTransformer(obj)).toBe(obj); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /spec/runtime/app_config.spec.js: -------------------------------------------------------------------------------- 1 | var Path = require('path'); 2 | 3 | var appConfig = require('runtime/app_config'); 4 | 5 | describe('appConfig', function() { 6 | it('returns always the root of this project as the basedir', function() { 7 | expect(appConfig.get('basedir')).toEqual(Path.resolve('.')); 8 | }); 9 | 10 | it('returns always "git" as the version control system', function() { 11 | expect(appConfig.get('versionControlSystem')).toEqual('git'); 12 | }); 13 | 14 | it('returns the default max concurrency value', function() { 15 | expect(appConfig.get('serialProcessing')).toBe(false); 16 | }); 17 | 18 | it('returns the default debug mode value', function() { 19 | expect(appConfig.get('debugMode')).toBe(false); 20 | }); 21 | 22 | it('returns the default log enabled value', function() { 23 | expect(appConfig.get('logEnabled')).toBe(true); 24 | }); 25 | 26 | it('returns the default server port value', function() { 27 | expect(appConfig.get('serverPort')).toEqual(3000); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /spec/runtime/env_config_reader.spec.js: -------------------------------------------------------------------------------- 1 | var EnvConfigReader = require('runtime/env_config_reader'); 2 | 3 | describe('EnvConfigReader', function() { 4 | beforeEach(function() { 5 | delete process.env.SERIAL_PROCESSING; 6 | delete process.env.COMMAND_DEBUG; 7 | delete process.env.LOG_DISABLED; 8 | delete process.env.SERVER_PORT; 9 | delete process.env.CODEMAAT_OPTS; 10 | }); 11 | 12 | describe('when no environment variables exist', function() { 13 | it('returns an object with default values', function() { 14 | expect(new EnvConfigReader().getConfiguration()).toEqual({ 15 | serialProcessing: false, 16 | debugMode: false, 17 | logEnabled: true, 18 | serverPort: undefined, 19 | codeMaat: { options: {} } 20 | }); 21 | }); 22 | }); 23 | 24 | describe('when environment variables exist', function() { 25 | it('returns an object with a max concurrency equal to 1', function() { 26 | process.env.SERIAL_PROCESSING = 'true'; 27 | 28 | expect(new EnvConfigReader().getConfiguration().serialProcessing).toBe(true); 29 | }); 30 | 31 | it('returns an object with a debug mode value', function() { 32 | process.env.COMMAND_DEBUG = '1'; 33 | 34 | expect(new EnvConfigReader().getConfiguration().debugMode).toBe(true); 35 | }); 36 | 37 | it('returns an object with a log enabled value', function() { 38 | process.env.LOG_DISABLED = '1'; 39 | 40 | expect(new EnvConfigReader().getConfiguration().logEnabled).toBe(false); 41 | }); 42 | 43 | it('returns an object with a server port value', function() { 44 | process.env.SERVER_PORT = '1234'; 45 | 46 | expect(new EnvConfigReader().getConfiguration().serverPort).toEqual(1234); 47 | }); 48 | 49 | it('returns an object with codeMaat options values', function() { 50 | process.env.CODEMAAT_OPTS = '-a 123 -b zxc'; 51 | 52 | expect(new EnvConfigReader().getConfiguration().codeMaat.options).toEqual({ 53 | '-a': '123', 54 | '-b': 'zxc' 55 | }); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /spec/runtime/file_config_reader.spec.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var FileConfigReader = require('runtime/file_config_reader'), 4 | utils = require('utils'); 5 | 6 | jest.mock('fs'); 7 | 8 | describe('FileConfigReader', function() { 9 | var subject; 10 | 11 | beforeEach(function() { 12 | subject = new FileConfigReader(); 13 | utils.fileSystem.isFile = jest.fn(); 14 | }); 15 | 16 | describe('when no configuration file exists', function() { 17 | it('returns an empty object', function() { 18 | utils.fileSystem.isFile.mockReturnValue(false); 19 | 20 | expect(subject.getConfiguration()).toEqual({}); 21 | }); 22 | }); 23 | 24 | describe('when a configuration file exists', function() { 25 | it('returns the parsed object from the json file', function() { 26 | utils.fileSystem.isFile.mockReturnValue(true); 27 | fs.readFileSync.mockReturnValue('{ "a": 1, "b": 2, "c": { "z": "qwe" } }'); 28 | 29 | expect(subject.getConfiguration()).toEqual({ a: 1, b: 2, c: { z: 'qwe' } }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /spec/tasks/__snapshots__/code_analysis_tasks.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Code analysis tasks sloc-report as a Function writes a report on the number of lines of code for each file in the repository: sloc-report.json 1`] = ` 4 | "[ 5 | {\\"path\\":\\"test_file1.js\\",\\"sourceLines\\":2,\\"totalLines\\":2}, 6 | {\\"path\\":\\"test_file2.rb\\",\\"sourceLines\\":3,\\"totalLines\\":3} 7 | ] 8 | " 9 | `; 10 | 11 | exports[`Code analysis tasks sloc-report as a Task writes a report on the number of lines of code for each file in the repository: sloc-report.json 1`] = ` 12 | "[ 13 | {\\"path\\":\\"test_file1.js\\",\\"sourceLines\\":2,\\"totalLines\\":2}, 14 | {\\"path\\":\\"test_file2.rb\\",\\"sourceLines\\":3,\\"totalLines\\":3} 15 | ] 16 | " 17 | `; 18 | 19 | exports[`Code analysis tasks sloc-trend-analysis publishes an analysis on the sloc trend for a given file in the repository: 2015-03-01_2015-10-22_sloc-trend-data.json 1`] = ` 20 | Array [ 21 | Object { 22 | "date": "2015-04-29T23:00:00.000Z", 23 | "path": "test_abs.rb", 24 | "revision": 123, 25 | "sourceLines": 3, 26 | "totalLines": 3, 27 | }, 28 | Object { 29 | "date": "2015-05-04T23:00:00.000Z", 30 | "path": "test_abs.rb", 31 | "revision": 456, 32 | "sourceLines": 5, 33 | "totalLines": 5, 34 | }, 35 | ] 36 | `; 37 | 38 | exports[`Code analysis tasks sloc-trend-analysis publishes an analysis on the sloc trend for a given file in the repository: manifest 1`] = ` 39 | Object { 40 | "dataFiles": Array [ 41 | Object { 42 | "fileUrl": "dbe9f7623278c6f5b3acbd89c20b4b5d70661491/2015-03-01_2015-10-22_sloc-trend-data.json", 43 | "timePeriod": "2015-03-01_2015-10-22", 44 | }, 45 | ], 46 | "dateRange": "2015-03-01_2015-10-22", 47 | "enabledDiagrams": Array [ 48 | "sloc", 49 | ], 50 | "id": "dbe9f7623278c6f5b3acbd89c20b4b5d70661491", 51 | "parameters": Object { 52 | "targetFile": "test_abs.rb", 53 | }, 54 | "reportName": "sloc-trend", 55 | "taskName": "sloc-trend-analysis", 56 | "time": "2015-10-22T10:00:00.000Z", 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /spec/tasks/__snapshots__/misc_tasks.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Misc Tasks generate-layer-grouping-files with the layer group parameter as a Function generates layer grouping files: layer-group-test-layer-1.txt 1`] = ` 4 | "test/path1 => Test Layer1 5 | ^test\\\\/path2\\\\/((?!.*--abc\\\\.)).*\\\\/files$ => Test Layer1" 6 | `; 7 | 8 | exports[`Misc Tasks generate-layer-grouping-files with the layer group parameter as a Function generates layer grouping files: layer-group-test-layer-2.txt 1`] = ` 9 | "test_path3 => Test Layer2 10 | ^test\\\\/path4\\\\/.*\\\\.cf$ => Test Layer2" 11 | `; 12 | 13 | exports[`Misc Tasks generate-layer-grouping-files with the layer group parameter as a Function generates layer grouping files: layer-groups.txt 1`] = ` 14 | "test/path1 => Test Layer1 15 | ^test\\\\/path2\\\\/((?!.*--abc\\\\.)).*\\\\/files$ => Test Layer1 16 | test_path3 => Test Layer2 17 | ^test\\\\/path4\\\\/.*\\\\.cf$ => Test Layer2" 18 | `; 19 | 20 | exports[`Misc Tasks generate-layer-grouping-files with the layer group parameter as a Task generates layer grouping files: layer-group-test-layer-1.txt 1`] = ` 21 | "test/path1 => Test Layer1 22 | ^test\\\\/path2\\\\/((?!.*--abc\\\\.)).*\\\\/files$ => Test Layer1" 23 | `; 24 | 25 | exports[`Misc Tasks generate-layer-grouping-files with the layer group parameter as a Task generates layer grouping files: layer-group-test-layer-2.txt 1`] = ` 26 | "test_path3 => Test Layer2 27 | ^test\\\\/path4\\\\/.*\\\\.cf$ => Test Layer2" 28 | `; 29 | 30 | exports[`Misc Tasks generate-layer-grouping-files with the layer group parameter as a Task generates layer grouping files: layer-groups.txt 1`] = ` 31 | "test/path1 => Test Layer1 32 | ^test\\\\/path2\\\\/((?!.*--abc\\\\.)).*\\\\/files$ => Test Layer1 33 | test_path3 => Test Layer2 34 | ^test\\\\/path4\\\\/.*\\\\.cf$ => Test Layer2" 35 | `; 36 | -------------------------------------------------------------------------------- /spec/tasks/helpers/code_maat_helper.spec.js: -------------------------------------------------------------------------------- 1 | var Helper = require('tasks/helpers/code_maat_helper'), 2 | codeMaat = require('analysers/code_maat'); 3 | 4 | describe('CodeMaatHelper', function() { 5 | var subject, mockAnalyser; 6 | beforeEach(function() { 7 | subject = new Helper({}); 8 | mockAnalyser = { 9 | isSupported: jest.fn().mockReturnValue('support-info'), 10 | fileAnalysisStream: jest.fn().mockReturnValue('test_result') 11 | }; 12 | codeMaat.analyser = jest.fn().mockReturnValue(mockAnalyser); 13 | }); 14 | 15 | Object.entries({ 16 | 'revisions': 'revisionsAnalysis', 17 | 'summary': 'summaryAnalysis', 18 | 'soc': 'sumCouplingAnalysis', 19 | 'coupling': 'temporalCouplingAnalysis', 20 | 'authors': 'authorsAnalysis', 21 | 'main-dev': 'mainDevAnalysis', 22 | 'entity-effort': 'effortAnalysis', 23 | 'entity-ownership': 'codeOwnershipAnalysis', 24 | 'communication': 'communicationAnalysis', 25 | 'abs-churn': 'absoluteChurnAnalysis', 26 | 'entity-churn': 'entityChurnAnalysis' 27 | }).forEach(function(entry) { 28 | var instruction = entry[0], analysis = entry[1]; 29 | it('returns information on vcs support', function() { 30 | expect(subject[analysis].isSupported()).toEqual('support-info'); 31 | }); 32 | 33 | it('returns the codemaat analysis of the input file', function() { 34 | var result = subject[analysis]('test_log_input', 'test_group_input', 'test_options'); 35 | 36 | expect(result).toEqual('test_result'); 37 | expect(codeMaat.analyser).toHaveBeenCalledWith(instruction); 38 | expect(mockAnalyser.fileAnalysisStream).toHaveBeenCalledWith('test_log_input', 'test_group_input', 'test_options'); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /spec/tasks/system_analysis/churn_metrics.spec.js: -------------------------------------------------------------------------------- 1 | var churnMetrics = require('tasks/system_analysis/churn_metrics'); 2 | 3 | describe('ChurnMetrics', function() { 4 | describe('selector', function() { 5 | it('returns the loc metrics with their total', function() { 6 | expect(churnMetrics.selector({ addedLines: 14, deletedLines: 10 })).toEqual({ 7 | addedLines: 14, 8 | deletedLines: 10, 9 | totalLines: 4 10 | }); 11 | }); 12 | 13 | it('returns the loc metrics reset to zero when the input metrics are not numbers', function() { 14 | expect(churnMetrics.selector({ addedLines: NaN, deletedLines: NaN })).toEqual({ 15 | addedLines: 0, 16 | deletedLines: 0, 17 | totalLines: 0 18 | }); 19 | }); 20 | }); 21 | 22 | it('returns the metrics initial values initialized to 0', function() { 23 | expect(churnMetrics.defaultValue).toEqual({ 24 | addedLines: 0, 25 | deletedLines: 0, 26 | totalLines: 0 27 | }); 28 | }); 29 | 30 | it('returns the cumulative metric for the total lines', function() { 31 | expect(churnMetrics.accumulatorsMap.cumulativeLines({ 32 | addedLines: 14, 33 | deletedLines: 10, 34 | totalLines: 4 35 | })).toEqual(4); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /spec/tasks/system_analysis/coupling_metrics.spec.js: -------------------------------------------------------------------------------- 1 | var couplingMetrics = require('tasks/system_analysis/coupling_metrics'); 2 | 3 | describe('CouplingMetrics', function() { 4 | it('returns the coupling metrics', function() { 5 | expect(couplingMetrics.selector({ coupledPath: 'test/path', couplingDegree: 10 })).toEqual({ 6 | coupledName: 'test/path', 7 | couplingDegree: 10 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /spec/tasks/system_analysis/summary_metrics.spec.js: -------------------------------------------------------------------------------- 1 | var summaryMetrics = require('tasks/system_analysis/summary_metrics'); 2 | 3 | describe('SummaryMetrics', function() { 4 | it('returns a metric value including only the given properties', function() { 5 | expect(summaryMetrics.selector({ stat: 'revisions', value: 10 })).toEqual({ revisions: 10 }); 6 | expect(summaryMetrics.selector({ stat: 'commits', value: 4 })).toEqual({ commits: 4 }); 7 | expect(summaryMetrics.selector({ stat: 'authors', value: 6 })).toEqual({ authors: 6 }); 8 | expect(summaryMetrics.selector({ stat: 'files', value: 3 })).toEqual({}); 9 | }); 10 | 11 | it('returns the metric initial value initialized to 0', function() { 12 | expect(summaryMetrics.defaultValue).toEqual({ revisions: 0, commits: 0, authors: 0 }); 13 | }); 14 | 15 | it('maps the metric accumulators to the given properties', function() { 16 | var item = { revisions: 7, commits: 4, authors: 5 }; 17 | 18 | expect(summaryMetrics.accumulatorsMap.cumulativeRevisions(item)).toEqual(7); 19 | expect(summaryMetrics.accumulatorsMap.cumulativeCommits(item)).toEqual(4); 20 | expect(summaryMetrics.accumulatorsMap.cumulativeAuthors(item)).toEqual(5); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /spec/utils/arrays.spec.js: -------------------------------------------------------------------------------- 1 | var arraysUtils = require('utils').arrays; 2 | 3 | describe('utils.arrays', function() { 4 | describe('.arrayToFnFactory()', function() { 5 | it('returns an array of wrapper functions around each array item handler', function() { 6 | var fnFactory = arraysUtils.arrayToFnFactory([1, 2, 3, 4], function(number) { return number * number; }); 7 | 8 | [1, 4, 9, 16].forEach(function(squaredElement, index) { 9 | expect(fnFactory[index]()).toEqual(squaredElement); 10 | }); 11 | }); 12 | }); 13 | 14 | describe('arrayPairsToObject', function() { 15 | it('returns an object from the key-value pairs extracted from the array', function() { 16 | expect(arraysUtils.arrayPairsToObject(['a', 123, 'b', 456])).toEqual({ 17 | a: 123, b: 456 18 | }); 19 | }); 20 | 21 | it('discards the last element when the array length is odd', function() { 22 | expect(arraysUtils.arrayPairsToObject(['a', 123, 'b', 456, 'c'])).toEqual({ 23 | a: 123, b: 456 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /spec/utils/definitions_archive.spec.js: -------------------------------------------------------------------------------- 1 | var DefinitionsArchive = require('utils').DefinitionsArchive; 2 | 3 | describe('utils.DefinitionsArchive', function() { 4 | it('archives definitions', function() { 5 | var archive = new DefinitionsArchive(); 6 | archive.addDefinition('testD1', {obj: 'definition1'}); 7 | 8 | expect(archive.getDefinition('testD1')).toEqual({obj: 'definition1'}); 9 | expect(archive.getDefinition('testD2')).toBeUndefined(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /spec/utils/file_system.spec.js: -------------------------------------------------------------------------------- 1 | // var fs = require('fs'); 2 | 3 | var fsUtils = require('utils').fileSystem; 4 | 5 | // jest.mock('fs'); 6 | var fs; 7 | jest.isolateModules(function() { 8 | fs = require('fs'); 9 | }); 10 | jest.mock('fs'); 11 | 12 | describe('utils.fileSystem', function() { 13 | var mockStat; 14 | beforeEach(function() { 15 | mockStat = { 16 | isFile: jest.fn(), 17 | isDirectory: jest.fn() 18 | }; 19 | fs.existsSync = jest.fn(); 20 | fs.statSync.mockReturnValue(mockStat); 21 | }); 22 | 23 | describe.each([ 24 | ['isFile'], ['isDirectory'] 25 | ])('Verify path', function(query) { 26 | it('returns true for an existing and valid path', function() { 27 | fs.existsSync.mockReturnValue(true); 28 | mockStat[query].mockReturnValue(true); 29 | 30 | expect(fsUtils[query]('test-path')).toBe(true); 31 | }); 32 | 33 | it('returns false for a non existing path', function() { 34 | fs.existsSync.mockReturnValue(false); 35 | expect(fsUtils[query]('test-path')).toBe(false); 36 | }); 37 | 38 | it('returns false for an existing but invalid path', function() { 39 | fs.existsSync.mockReturnValue(true); 40 | mockStat[query].mockReturnValue(false); 41 | expect(fsUtils[query]('test-path')).toBe(false); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /spec/utils/xml_utils.spec.js: -------------------------------------------------------------------------------- 1 | var XmlUtils = require('utils/xml_utils'); 2 | 3 | describe('XmlUtils', function() { 4 | describe('nodeWithName()', function() { 5 | it('returns a resolver for the xml tag with the given name', function() { 6 | var fn = XmlUtils.nodeWithName('test_name'); 7 | 8 | expect(fn({ name: 'test_name' })).toEqual(true); 9 | expect(fn({ name: 'another_name' })).toEqual(false); 10 | }); 11 | }); 12 | 13 | describe('nodeText()', function() { 14 | it('returns text content of the given xml node', function() { 15 | var node = { 16 | children: [ 17 | { type: 'attribute', value: 'test_attribute' }, 18 | { type: 'text', value: 'test_text' } 19 | ] 20 | }; 21 | 22 | expect(XmlUtils.nodeText(node)).toEqual('test_text'); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /spec/web/diagrams/data_proxy.spec.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | 3 | var DataProxy = require('web/diagrams/data_proxy'); 4 | 5 | describe('DataProxy', function() { 6 | describe('default layout adapter', function() { 7 | it('does not change the data for the layout', function() { 8 | var proxy = new DataProxy(null, function(data) { 9 | return 'transformed ' + data; 10 | }); 11 | 12 | return proxy.processData('test-data') 13 | .then(function(result) { 14 | expect(result).toEqual('transformed test-data'); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('default transform function', function() { 20 | it('returns the same data from the layout adapter', function() { 21 | var adapter = { 22 | toSeries: Bluebird.method(function(data) { 23 | return data * 2; 24 | }) 25 | }; 26 | var proxy = new DataProxy(adapter); 27 | 28 | return expect(proxy.processData(123)).resolves.toEqual(246); 29 | }); 30 | }); 31 | }); 32 | --------------------------------------------------------------------------------