fastlane-plugin-code_static_analyzer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ #!/bin/bash -o pipefail
2
+ #!/bin/sh -e
3
+ # run_script.sh
4
+ # @desc Run shell script.
5
+ # @usage
6
+ # Parameters
7
+ # $1 - shell script you want to run
8
+ # $2 - filename with extension to store command results
9
+
10
+ {
11
+ eval $1 | tee "$2"
12
+ } &> /dev/null
13
+ exit $?
@@ -0,0 +1,17 @@
1
+ require 'fastlane/plugin/code_static_analyzer/version'
2
+
3
+ module Fastlane
4
+ module CodeStaticAnalyzer
5
+ # Return all .rb files inside the "actions" and "helper" directory
6
+ ROOT = Pathname.new(File.expand_path('../../..', __FILE__))
7
+ def self.all_classes
8
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
9
+ end
10
+ end
11
+ end
12
+
13
+ # By default we want to import all available actions and helpers
14
+ # A plugin can contain any number of actions and plugins
15
+ Fastlane::CodeStaticAnalyzer.all_classes.each do |current|
16
+ require current
17
+ end
@@ -0,0 +1,310 @@
1
+ module Fastlane
2
+ require File.join CodeStaticAnalyzer::ROOT, "assets/formatter.rb"
3
+ require File.join CodeStaticAnalyzer::ROOT, "assets/junit_parser.rb"
4
+ require 'xcodeproj'
5
+
6
+ module Actions
7
+ module SharedValues
8
+ ANALYZER_STATUS = :ANALYZER_STATUS
9
+ end
10
+
11
+ class CodeStaticAnalyzerAction < Action
12
+ SUPPORTED_ANALYZER = ["xcodeWar", "rubocop", "CPD"]
13
+ RESULT_FILE = 'codeAnalysResults_analyzers.xml'
14
+ attr_accessor :checked_pmd, :checked_xcode_param, :xml_content, :run_main
15
+
16
+ def self.run(params)
17
+ @run_main = true
18
+ Actions::CodeStaticAnalyzerAction.is_pmd_installed
19
+ platform = Actions.lane_context[SharedValues::PLATFORM_NAME].to_s
20
+ @xml_content = ''
21
+ root_dir = work_dir
22
+ analyzers = params[:analyzers]
23
+ analyzers = SUPPORTED_ANALYZER if (analyzers and analyzers.empty?) or analyzers[0] == 'all'
24
+ xcode_project = params[:xcode_project_name]
25
+ xcode_workspace = params[:xcode_workspace_name]
26
+ xcode_targets = params[:xcode_targets]
27
+ # use additional checks for optional parameters, but required in specific analyzer
28
+ analyzers.each do |analyzer|
29
+ case analyzer.downcase
30
+ when 'xcodewar'
31
+ UI.user_error!("No project name for Warnings Analyzer given. Pass using `xcode_project` or configure analyzers to run using `analyzers`") if !xcode_project or (xcode_project and xcode_project.empty?) and platform != 'android'
32
+ checked_params = xcode_check_parameters(root_dir, xcode_project, xcode_workspace, xcode_targets)
33
+ xcode_project = checked_params[0]
34
+ xcode_workspace = checked_params[1]
35
+ xcode_targets = checked_params[2]
36
+ end
37
+ end
38
+
39
+ status_rubocop = 0
40
+ status_static = 0
41
+ clear_all_files = "#{root_dir}#{params[:result_dir]}/*.*"
42
+ # clear_temp_files = "#{root_dir}#{params[:result_dir]}/*temp*.*"
43
+ sh "rm -rf #{clear_all_files}"
44
+
45
+ # Run alyzers
46
+ status_cpd = Actions::CpdAnalyzerAction.run(
47
+ result_dir: params[:result_dir],
48
+ tokens: params[:cpd_tokens],
49
+ language: params[:cpd_language],
50
+ cpd_files_to_inspect: params[:cpd_files_to_inspect],
51
+ cpd_files_to_exclude: params[:cpd_files_to_exclude]
52
+ )
53
+ analyzers.each do |analyzer|
54
+ case analyzer.downcase
55
+ when 'xcodewar'
56
+ if platform != "android"
57
+ status_static = Actions::WarningAnalyzerAction.run(
58
+ result_dir: params[:result_dir],
59
+ xcode_project_name: xcode_project,
60
+ xcode_workspace_name: xcode_workspace,
61
+ xcode_targets: xcode_targets
62
+ )
63
+ end
64
+ when 'rubocop'
65
+ status_rubocop = Actions::RubyAnalyzerAction.run(
66
+ result_dir: params[:result_dir],
67
+ ruby_files: params[:ruby_files]
68
+ )
69
+ end
70
+ end
71
+ # sh "rm -rf #{clear_temp_files}"
72
+
73
+ create_analyzers_run_result("#{root_dir}#{params[:result_dir]}/") unless @xml_content.empty?
74
+
75
+ if Actions::CodeStaticAnalyzerAction.status_to_boolean(status_cpd) &&
76
+ Actions::CodeStaticAnalyzerAction.status_to_boolean(status_static) &&
77
+ Actions::CodeStaticAnalyzerAction.status_to_boolean(status_rubocop)
78
+ Actions.lane_context[SharedValues::ANALYZER_STATUS] = true
79
+ else
80
+ Actions.lane_context[SharedValues::ANALYZER_STATUS] = false
81
+ end
82
+ end
83
+
84
+ def self.start_xml_content
85
+ @xml_content = ""
86
+ end
87
+
88
+ def self.run_from_main_action
89
+ @run_main
90
+ end
91
+
92
+ def self.analyzers_xml
93
+ @xml_content
94
+ end
95
+
96
+ class << self
97
+ attr_reader :checked_pmd
98
+ end
99
+
100
+ def self.checked_xcode
101
+ @checked_xcode_param
102
+ end
103
+
104
+ def self.add_xml_content(root, analyzer_name, temp_result_file, info)
105
+ UI.error "#{analyzer_name} analyzer failed to create temporary result file. More info in #{root}#{RESULT_FILE}"
106
+ new_line = JunitParser.xml_level(3)
107
+ if info.empty?
108
+ info = "Don't see #{temp_result_file} file.#{new_line}Try to run command with --verbose" \
109
+ " to see warnings made by used analyzers"
110
+ end
111
+ failures = JunitParser.add_failure('', 'unexisted result file', "#{new_line}#{info}")
112
+ @xml_content += JunitParser.add_failed_testcase("#{analyzer_name} analyzer crash", failures)
113
+ end
114
+
115
+ def self.create_analyzers_run_result(result_dir)
116
+ unless @xml_content.empty?
117
+ junit_xml = JunitParser.add_testsuite('static anlyzers', @xml_content)
118
+ JunitParser.create_junit_xml(junit_xml, "#{result_dir}/#{RESULT_FILE}")
119
+ end
120
+ end
121
+
122
+ def self.status_to_boolean(var)
123
+ if var == 0 or var == '0' or var == true or var == 'true'
124
+ return true
125
+ else
126
+ return false
127
+ end
128
+ end
129
+
130
+ def self.is_pmd_installed
131
+ @checked_pmd = false
132
+ begin
133
+ Actions.sh('type pmd')
134
+ rescue
135
+ UI.user_error! 'PMD not installed. Please, install PMD for using copy paste analyzer.'
136
+ end
137
+ @checked_pmd = true
138
+ end
139
+
140
+ def self.work_dir
141
+ directory = Dir.pwd
142
+ directory + "/" unless directory.end_with? "/"
143
+ end
144
+
145
+ def self.check_file_exist(work_dir, file, parameter_name)
146
+ if file.kind_of?(Array)
147
+ file.each do |file_path|
148
+ UI.user_error!("Unexisted path '#{work_dir}#{file_path}'. Check '#{parameter_name}' parameter. Files should be relative to work directory '#{work_dir}'") if Dir.glob("#{work_dir}#{file_path}").empty?
149
+ end
150
+ else
151
+ UI.user_error!("Unexisted path '#{work_dir}#{file}'. Check '#{parameter_name}' parameter. Files should be relative to work directory '#{work_dir}'") if Dir.glob("#{work_dir}#{file}").empty?
152
+ end
153
+ end
154
+
155
+ def self.add_root_path(root, file_list, is_inspected)
156
+ file_list_str = ''
157
+ if file_list.nil? || file_list.empty?
158
+ file_list_str = "'#{root}'" if is_inspected
159
+ else
160
+ file_list.each do |file|
161
+ file_path = "#{root}#{file}"
162
+ file_path = file_path.sub("//", '/')
163
+ file_path = file_path.sub("/./", '/')
164
+ file_list_str += "'#{file_path}' "
165
+ end
166
+ end
167
+ file_list_str
168
+ end
169
+
170
+ def self.xcode_check_parameters(root_dir, project, workspace, targets)
171
+ @checked_xcode_param = false
172
+ if Actions.lane_context[SharedValues::PLATFORM_NAME] == 'android'
173
+ UI.user_error! 'This warning_analyzer not supported for ios platform'
174
+ else
175
+ unless project.empty?
176
+ project += '.xcodeproj' unless project.end_with? '.xcodeproj'
177
+ wrong_path = Dir.glob(root_dir + project).empty?
178
+ UI.user_error! "Wrong project name '#{project}'. Check extension if you use it." if wrong_path
179
+ end
180
+
181
+ if workspace and !workspace.empty?
182
+ workspace += '.xcworkspace' unless workspace.end_with? '.xcworkspace'
183
+ else
184
+ workspace = project.gsub(".xcodeproj", '') + '.xcworkspace'
185
+ end
186
+ wrong_path = Dir.glob(root_dir + workspace).empty?
187
+ UI.user_error! "Wrong workspace name '#{workspace}'" if wrong_path
188
+
189
+ # check targets
190
+ available_targets = []
191
+ new_targets = []
192
+ project_info = Xcodeproj::Project.open(project.to_s)
193
+ project_info.targets.each do |target|
194
+ available_targets.push(target.name)
195
+ end
196
+ if targets and targets.count > 0
197
+ error = []
198
+ targets.each do |currect_target|
199
+ next if currect_target.tr(' ', '').empty?
200
+ if available_targets.include? currect_target
201
+ new_targets.push(currect_target)
202
+ else
203
+ error.push(currect_target)
204
+ end
205
+ end
206
+ UI.user_error!("The #{error} targets don't exist in project.") unless error.count == 0
207
+ end
208
+ new_targets = available_targets if new_targets.count == 0
209
+ @checked_xcode_param = true
210
+ [project, workspace, new_targets]
211
+ end
212
+ end
213
+
214
+ def self.description
215
+ "Runs different Static Analyzers and generate report"
216
+ end
217
+
218
+ def self.authors
219
+ ["olgakn"]
220
+ end
221
+
222
+ def self.return_value
223
+ # If your method provides a return value, you can describe here what it does
224
+ end
225
+
226
+ def self.details
227
+ # Optional:
228
+ "This plugins is the helper for checking code on warnings, copypaste, syntax, etc.\n" \
229
+ "Each analyzer in this plugin save result status in shared values <NAME>_ANALYZER_STATUS: 0 - code is clear, any other value - code include warnings/errors.\n" \
230
+ "Also each analyzer save results in separate file: codeAnalysResult_<name of analyzer>.xml"
231
+ end
232
+
233
+ def self.available_options
234
+ [
235
+ FastlaneCore::ConfigItem.new(key: :analyzers,
236
+ env_name: "FL_CSA_RUN_ANALYZERS",
237
+ description: "List of analysers you want to run. Supported analyzers: #{SUPPORTED_ANALYZER}",
238
+ optional: false,
239
+ type: Array,
240
+ verify_block: proc do |value|
241
+ UI.message '[!] Will be run all analyzers'.blue if (value and value.empty?) or value[0] == 'all'
242
+ value.each do |run_analyzer|
243
+ UI.user_error!("The analyzer '#{run_analyzer}' is not supported. Supported analyzers: #{SUPPORTED_ANALYZER}, 'all'") unless SUPPORTED_ANALYZER.map(&:downcase).include? run_analyzer.downcase or run_analyzer == 'all'
244
+ end
245
+ end),
246
+ FastlaneCore::ConfigItem.new(key: :result_dir,
247
+ env_name: "CSA_RESULT_DIR_NAME",
248
+ description: "Directory's name for storing analysis results",
249
+ optional: true,
250
+ type: String,
251
+ default_value: 'artifacts'),
252
+ # parameters for CPD analyzer
253
+ FastlaneCore::ConfigItem.new(key: :cpd_tokens,
254
+ description: "The min number of words in code that is detected as copy paste",
255
+ optional: true,
256
+ type: String,
257
+ default_value: '100'),
258
+ FastlaneCore::ConfigItem.new(key: :cpd_files_to_inspect,
259
+ description: "List of files and directories (relative to work directory) to inspect on copy paste",
260
+ optional: true,
261
+ type: Array),
262
+ FastlaneCore::ConfigItem.new(key: :cpd_files_to_exclude,
263
+ description: "List of files and directories (relative to work directory) not to inspect on copy paste",
264
+ optional: true,
265
+ type: Array),
266
+ FastlaneCore::ConfigItem.new(key: :cpd_language,
267
+ description: "Language used in files that will be inspected on copy paste.\nSupported analyzers: #{Actions::CpdAnalyzerAction::SUPPORTED_LAN} or don't set if you need any other language",
268
+ optional: true,
269
+ type: String,
270
+ verify_block: proc do |value|
271
+ UI.user_error!("Language '#{value}' is not supported.\nSupported languages: #{Actions::CpdAnalyzerAction::SUPPORTED_LAN} or empty if you need any other language") unless Actions::CpdAnalyzerAction::SUPPORTED_LAN.map(&:downcase).include? value.downcase or value.empty? or !value
272
+ end),
273
+ # parameters for Ruby analyzer
274
+ FastlaneCore::ConfigItem.new(key: :ruby_files,
275
+ description: "List of path (relative to work directory) to ruby files to be inspected on warnings & syntax",
276
+ optional: true,
277
+ type: Array),
278
+ # parameters for Warnings analyzer
279
+ FastlaneCore::ConfigItem.new(key: :xcode_project_name, # required in analyzer
280
+ description: "Xcode project name in work directory",
281
+ optional: true,
282
+ type: String),
283
+ FastlaneCore::ConfigItem.new(key: :xcode_workspace_name,
284
+ description: "Xcode workspace name in work directory",
285
+ optional: true,
286
+ type: String),
287
+ FastlaneCore::ConfigItem.new(key: :xcode_targets,
288
+ description: "List of Xcode targets to inspect",
289
+ optional: true,
290
+ type: Array)
291
+ ]
292
+ end
293
+
294
+ def self.output
295
+ # Define the shared values you are going to provide
296
+ [
297
+ ['ANALYZER_STATUS', 'Code analysis result (0 - code is clear, any other value - code include warnings/errors/etc.)']
298
+ ]
299
+ end
300
+
301
+ def self.is_supported?(platform)
302
+ # Adjust this if your plugin only works for a particular platform (iOS vs. Android, for example)
303
+ # See: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md
304
+ #
305
+ # [:ios, :mac, :android].include?(platform)
306
+ true
307
+ end
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,134 @@
1
+ module Fastlane
2
+ module Actions
3
+ module SharedValues
4
+ CPD_ANALYZER_STATUS = :CPD_ANALYZER_STATUS
5
+ end
6
+
7
+ class CpdAnalyzerAction < Action
8
+ SUPPORTED_LAN = ['python', 'objectivec', 'jsp', 'ecmascript', 'fortran', 'cpp', 'ruby', 'php', 'java', 'matlab', 'scala', 'plsql', 'go', 'cs']
9
+
10
+ def self.run(params)
11
+ UI.header 'CPD analyzer' if Actions::CodeStaticAnalyzerAction.run_from_main_action
12
+ Actions::CodeStaticAnalyzerAction.is_pmd_installed unless Actions::CodeStaticAnalyzerAction.checked_pmd
13
+ work_dir = Actions::CodeStaticAnalyzerAction.work_dir
14
+
15
+ # checking files for analysing
16
+ files_to_exclude = params[:cpd_files_to_exclude]
17
+ files_to_inspect = params[:cpd_files_to_inspect]
18
+ UI.message '[!] CPD analyzer will be run for all files in work directory'.blue if !files_to_inspect or files_to_inspect.empty?
19
+ Actions::CodeStaticAnalyzerAction.check_file_exist(work_dir, files_to_exclude, 'cpd_files_to_exclude') if files_to_exclude
20
+ Actions::CodeStaticAnalyzerAction.check_file_exist(work_dir, files_to_inspect, 'cpd_files_to_inspect') if files_to_inspect
21
+
22
+ # prepare script and metadata for saving results
23
+ result_dir_path = "#{work_dir}#{params[:result_dir]}"
24
+ FileUtils.mkdir_p(result_dir_path) unless File.exist?(result_dir_path)
25
+ temp_result_file = "#{result_dir_path}/temp_copypaste.xml"
26
+ result_file = "#{result_dir_path}/codeAnalysResults_cpd.xml"
27
+ tokens = params[:tokens]
28
+ files = Actions::CodeStaticAnalyzerAction.add_root_path(work_dir, files_to_inspect, true)
29
+ lan = params[:language]
30
+ exclude_files = Actions::CodeStaticAnalyzerAction.add_root_path(work_dir, files_to_exclude, false)
31
+ run_script = " pmd cpd --minimum-tokens #{tokens} --files #{files}"
32
+ run_script += " --exclude #{exclude_files}" unless exclude_files == ''
33
+ run_script += " --language #{lan}" unless (lan and lan.empty?) or !lan
34
+ run_script += " --format xml"
35
+ run_script_path = File.join CodeStaticAnalyzer::ROOT, "assets/run_script.sh"
36
+ run_script = "#{run_script_path} \"#{run_script}\" '#{temp_result_file}'"
37
+ # use analyzer
38
+ Formatter.cpd_format(tokens, lan, exclude_files, temp_result_file, files)
39
+ FastlaneCore::CommandExecutor.execute(command: run_script.to_s,
40
+ print_all: false,
41
+ error: proc do |error_output|
42
+ # handle error here
43
+ end)
44
+ status = $?.exitstatus
45
+
46
+ # prepare results
47
+ if Dir.glob(temp_result_file).empty? or status == 1
48
+ Actions::CodeStaticAnalyzerAction.start_xml_content unless Actions::CodeStaticAnalyzerAction.run_from_main_action
49
+ info = (status == 1) ? "CPD return 1: Couldn't understand command line parameters or CPD exited with an exception" : ''
50
+ Actions::CodeStaticAnalyzerAction.add_xml_content("#{result_dir_path}/", 'Copy paste', temp_result_file, info)
51
+ Actions::CodeStaticAnalyzerAction.create_analyzers_run_result("#{result_dir_path}/") unless Actions::CodeStaticAnalyzerAction.run_from_main_action
52
+ status = 43
53
+ else
54
+ status = 0 if File.read(temp_result_file).empty?
55
+ xml_content = JunitParser.parse_xml(temp_result_file)
56
+ junit_xml = JunitParser.add_testsuite('copypaste', xml_content)
57
+ JunitParser.create_junit_xml(junit_xml, result_file)
58
+ end
59
+ Actions.lane_context[SharedValues::CPD_ANALYZER_STATUS] = status
60
+ end
61
+
62
+ #####################################################
63
+ # @!group Documentation
64
+ #####################################################
65
+
66
+ def self.description
67
+ "This analyzer detect copy paste code (it uses PMD CPD)"
68
+ end
69
+
70
+ def self.details
71
+ # Optional:
72
+ # this is your chance to provide a more detailed description of this action
73
+ "Important: install PMD if you want to use copy paste detector\n" \
74
+ "Important: Always use 'language' parameter except the needed language isn't available in list of supported languages"
75
+ end
76
+
77
+ def self.available_options
78
+ # Define all options your action supports.
79
+ [
80
+ FastlaneCore::ConfigItem.new(key: :result_dir,
81
+ env_name: "FL_CPD_ANALYZER_RESULT_DIR",
82
+ description: "Directory's name for storing analysis results",
83
+ optional: true,
84
+ type: String,
85
+ default_value: 'artifacts'),
86
+ FastlaneCore::ConfigItem.new(key: :tokens,
87
+ env_name: "FL_CPD_ANALYZER_TOKENS",
88
+ description: "The min number of words in code that is detected as copy paste",
89
+ optional: true,
90
+ type: String,
91
+ default_value: '100'),
92
+ FastlaneCore::ConfigItem.new(key: :cpd_files_to_inspect,
93
+ env_name: "FL_CPD_ANALYZER_FILES_TO_INSPECT",
94
+ description: "List of files and directories (relative to work directory) to inspect on copy paste",
95
+ optional: true,
96
+ type: Array),
97
+ FastlaneCore::ConfigItem.new(key: :cpd_files_to_exclude,
98
+ env_name: "FL_CPD_ANALYZER_FILES_NOT_TO_INSPECT",
99
+ description: "List of files and directories (relative to work directory) not to inspect on copy paste",
100
+ optional: true,
101
+ type: Array),
102
+ FastlaneCore::ConfigItem.new(key: :language,
103
+ env_name: "FL_CPD_ANALYZER_FILE_LANGUAGE",
104
+ description: "Language used in files that will be inspected on copy paste.\nSupported analyzers: #{SUPPORTED_LAN} or don't set if you need any other language",
105
+ optional: true,
106
+ type: String,
107
+ verify_block: proc do |value|
108
+ UI.user_error!("This language is not supported. Supported languages: #{SUPPORTED_LAN} or empty if you need any other language") unless SUPPORTED_LAN.map(&:downcase).include? value.downcase or value.empty? or !value
109
+ end)
110
+ ]
111
+ end
112
+
113
+ def self.output
114
+ # Define the shared values you are going to provide
115
+ [
116
+ ['CPD_ANALYZER_STATUS', 'Copy paste analyzer result status (0 - success, any other value - failed)']
117
+ ]
118
+ end
119
+
120
+ def self.return_value
121
+ # If you method provides a return value, you can describe here what it does
122
+ end
123
+
124
+ def self.authors
125
+ # So no one will ever forget your contribution to fastlane :) You are awesome btw!
126
+ ["olgakn"]
127
+ end
128
+
129
+ def self.is_supported?(platform)
130
+ true
131
+ end
132
+ end
133
+ end
134
+ end