Skip to content

Commit 98bac37

Browse files
committed
Python duplication support, fix reporting
* Adds ability to transform Python -> AST -> JSON -> S-Expression for flay. * Runs `Flay#report` only once now to fix exponential issue bug. * Only grabs the first reported location from flay and stores the rest as `other_locations`. * Now returns `end` line instead of the current line for both beginning and end. * Uses `_type` in the Python script to return JSON-ified AST since `type` is used by the AST. A large portion of this code was copied from the JavaScript and Ruby analyzers as well as a private Python repo.
1 parent 826ba27 commit 98bac37

File tree

15 files changed

+328
-57
lines changed

15 files changed

+328
-57
lines changed

Dockerfile

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ COPY Gemfile.lock /usr/src/app/
88
COPY vendor/php-parser/composer.json /usr/src/app/vendor/php-parser/
99
COPY vendor/php-parser/composer.lock /usr/src/app/vendor/php-parser/
1010

11-
RUN apk --update add nodejs ruby ruby-io-console ruby-dev ruby-bundler build-base \
12-
php-cli php-json php-phar php-openssl php-xml curl && \
11+
RUN apk --update add python nodejs php-cli php-json php-phar php-openssl php-xml curl\
12+
ruby ruby-io-console ruby-dev ruby-bundler build-base && \
1313
bundle install -j 4 && \
1414
apk del build-base && rm -fr /usr/share/ri && \
1515
curl -sS https://getcomposer.org/installer | php

Gemfile.lock

-3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,3 @@ DEPENDENCIES
3939
pry
4040
rake
4141
sexp_processor
42-
43-
BUNDLED WITH
44-
1.10.6

bin/duplication

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ else
99
engine_config = {}
1010
end
1111

12+
directory = ARGV[0] || "/code"
13+
1214
CC::Engine::Duplication.new(
13-
directory: "/code", engine_config: engine_config, io: STDOUT
15+
directory: directory, engine_config: engine_config, io: STDOUT
1416
).run

lib/cc/engine/analyzers/helpers/main.rb

+49-10
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,30 @@ module Analyzers
66
module Helpers
77
BASE_POINTS = 10_000
88

9-
def new_violation(issue, location, other)
9+
def parsed_hashes
10+
@parsed_hashes ||= []
11+
end
12+
13+
def flay
14+
@flay ||= ::Flay.new(flay_options)
15+
end
16+
17+
def report
18+
flay.report(StringIO.new).each do |issue|
19+
location = issue.locations.first
20+
io.puts "#{new_violation(issue, location).to_json}\0"
21+
end
22+
end
23+
24+
def new_violation(issue, location)
1025
{
1126
"type": "issue",
1227
"check_name": name(issue),
1328
"description": "Duplication found in #{issue.name}",
1429
"categories": ["Duplication"],
15-
"location": format_location(location),
30+
"location": format_location(issue, location),
1631
"remediation_points": calculate_points(issue),
17-
"other_locations": format_locations(other),
32+
"other_locations": format_locations(issue, location),
1833
"content": content_body
1934
}
2035
end
@@ -31,18 +46,42 @@ def find_other_locations(all_locations, current)
3146
all_locations.reject { |location| location == current }
3247
end
3348

34-
def format_location(location)
49+
def format_location(issue, location)
50+
current_sexp = flay.hashes[issue.structural_hash].detect do |sexp|
51+
sexp.line == location.line
52+
end
53+
54+
format_sexp(current_sexp)
55+
end
56+
57+
def format_locations(issue, location)
58+
sexps = flay.hashes[issue.structural_hash].reject do |sexp|
59+
sexp.line == location.line
60+
end
61+
62+
sexps.map do |sexp|
63+
format_sexp(sexp)
64+
end
65+
end
66+
67+
def format_sexp(sexp)
3568
{
36-
"path": local_path(location.file),
69+
"path": local_path(sexp.file),
3770
"lines": {
38-
"begin": location.line,
39-
"end": location.line
71+
"begin": sexp.line,
72+
"end": sexp_max_line(sexp, sexp.line)
4073
}
4174
}
4275
end
4376

44-
def format_locations(other)
45-
other.map { |location| format_location(location) }
77+
def sexp_max_line(sexp_tree, default)
78+
max = default
79+
80+
sexp_tree.deep_each do |sexp|
81+
max = sexp.line if sexp.line > max
82+
end
83+
84+
max
4685
end
4786

4887
def flay_options
@@ -64,7 +103,7 @@ def local_path(file)
64103
end
65104

66105
def directory_path
67-
@directory_path ||= Pathname.new(@directory).realpath.to_s
106+
@directory_path ||= Pathname.new(@directory).to_s
68107
end
69108

70109
def excluded_files

lib/cc/engine/analyzers/javascript/main.rb

-9
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,6 @@ def mass_threshold
4242
def start_flay(s_expressions)
4343
flay = ::Flay.new(flay_options)
4444
flay.process_sexp(s_expressions)
45-
46-
flay.report(StringIO.new).each do |issue|
47-
all_locations = issue.locations
48-
49-
all_locations.each do |location|
50-
other_locations = find_other_locations(all_locations, location)
51-
io.puts "#{new_violation(issue, location, other_locations).to_json}\0"
52-
end
53-
end
5445
end
5546

5647
def analyzed_files

lib/cc/engine/analyzers/javascript/parser.rb

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def parse
2323
def on_success(output)
2424
parsed_json = JSON.parse(output, max_nesting: false)
2525
parsed_json.delete('sourceType')
26+
27+
2628
@syntax_tree = CC::Engine::Analyzers::Javascript::AST.json_to_ast(parsed_json, filename)
2729
end
2830

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require 'cc/engine/analyzers/helpers/main'
2+
require 'cc/engine/analyzers/python/parser'
3+
require 'cc/engine/analyzers/python/node'
4+
require 'flay'
5+
6+
module CC
7+
module Engine
8+
module Analyzers
9+
module Python
10+
class Main
11+
include ::CC::Engine::Analyzers::Helpers
12+
13+
attr_reader :directory, :engine_config, :io
14+
15+
def initialize(directory:, engine_config:, io:)
16+
@directory = directory
17+
@engine_config = engine_config || {}
18+
@io = io
19+
end
20+
21+
def run
22+
files_to_analyze.each do |file|
23+
start_flay(process_file(file))
24+
end
25+
end
26+
27+
def process_file(path)
28+
Node.new(::CC::Engine::Analyzers::Python::Parser.new(File.binread(path), path).parse.syntax_tree, path).format
29+
end
30+
31+
def mass_threshold
32+
engine_config.fetch('config', {}).fetch('python', {}).fetch('mass_threshold', 50)
33+
end
34+
35+
def start_flay(s_expressions)
36+
return if s_expressions.nil?
37+
flay.process_sexp(s_expressions)
38+
end
39+
40+
def files_to_analyze
41+
files = Dir.glob("#{directory}/**/*.py").reject do |f|
42+
File.directory?(f)
43+
end
44+
45+
files - excluded_files
46+
end
47+
end
48+
end
49+
end
50+
end
51+
end
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
module CC
2+
module Engine
3+
module Analyzers
4+
module Python
5+
class Node
6+
SCRUB_PROPERTIES = ["_type", "attributes"].freeze
7+
8+
def initialize(node, file, default_line = 0)
9+
@node = node
10+
@file = file
11+
12+
set_default_line(default_line)
13+
end
14+
15+
def format
16+
if @node.is_a?(Hash)
17+
type = @node["_type"].to_sym
18+
19+
if valid_properties
20+
create_sexp(type, *properties_to_sexps)
21+
else
22+
type
23+
end
24+
elsif @node.is_a?(Array)
25+
@node.map do |n|
26+
Node.new(n, @file, @line).format
27+
end
28+
end
29+
end
30+
31+
private
32+
33+
def properties_to_sexps
34+
valid_properties.map do |key, value|
35+
if value.is_a?(Array) || value.is_a?(Hash)
36+
create_sexp(key.to_sym, *Node.new(value, @file, @line).format)
37+
else
38+
create_sexp(key.to_sym, value)
39+
end
40+
end
41+
end
42+
43+
def create_sexp(*args)
44+
Sexp.new(*args).tap do |sexp|
45+
sexp.file = @file
46+
sexp.line = @line
47+
end
48+
end
49+
50+
def valid_properties
51+
@node.reject do |key, value|
52+
value_empty = value == nil || value == {} || value == []
53+
SCRUB_PROPERTIES.include?(key) || value_empty
54+
end
55+
end
56+
57+
def set_default_line(default)
58+
if has_line_number?
59+
@line = @node["attributes"]["lineno"]
60+
else
61+
@line = default
62+
end
63+
end
64+
65+
def has_line_number?
66+
@node.is_a?(Hash) && @node["attributes"] && @node["attributes"]["lineno"]
67+
end
68+
end
69+
end
70+
end
71+
end
72+
end
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json, sys, ast
2+
3+
def to_json(node):
4+
json_ast = {'attributes': {}}
5+
json_ast['_type'] = node.__class__.__name__
6+
for key, value in ast.iter_fields(node):
7+
json_ast[key] = cast_value(value)
8+
for attr in node._attributes:
9+
json_ast['attributes'][attr] = cast_value(getattr(node, attr))
10+
return json_ast
11+
12+
def cast_infinity(value):
13+
if value > 0:
14+
return "Infinity"
15+
else:
16+
return "-Infinity"
17+
18+
def cast_value(value):
19+
if value is None or isinstance(value, (bool, basestring)):
20+
return value
21+
elif isinstance(value, (int, float, long, complex)):
22+
if abs(value) == 1e3000:
23+
return cast_infinity(value)
24+
return value
25+
elif isinstance(value, list):
26+
return [cast_value(v) for v in value]
27+
else:
28+
return to_json(value)
29+
30+
if __name__ == '__main__':
31+
source = ""
32+
for line in sys.stdin.readlines():
33+
source += line
34+
print json.dumps(to_json(ast.parse(source)))
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
require 'posix/spawn'
2+
require 'timeout'
3+
require 'json'
4+
5+
module CC
6+
module Engine
7+
module Analyzers
8+
module Python
9+
class Parser
10+
attr_reader :code, :filename, :syntax_tree
11+
12+
def initialize(code, filename)
13+
@code = code
14+
@filename = filename
15+
end
16+
17+
def parse
18+
runner = CommandLineRunner.new(python_env, self)
19+
runner.run(code) do |ast|
20+
json_ast = JSON.parse(ast)
21+
@syntax_tree = json_ast
22+
end
23+
24+
self
25+
end
26+
27+
def python_env
28+
file = File.expand_path(File.dirname(__FILE__)) + '/parser.py'
29+
"python #{file}"
30+
end
31+
end
32+
33+
class CommandLineRunner
34+
DEFAULT_TIMEOUT = 20
35+
36+
attr_reader :command, :delegate
37+
38+
def initialize(command, delegate)
39+
@command = command
40+
@delegate = delegate
41+
end
42+
43+
def run(input, timeout = DEFAULT_TIMEOUT)
44+
Timeout.timeout(timeout) do
45+
child = ::POSIX::Spawn::Child.new(command, input: input, timeout: timeout)
46+
47+
if child.status.success?
48+
yield child.out if block_given?
49+
end
50+
end
51+
end
52+
end
53+
end
54+
end
55+
end
56+
end

lib/cc/engine/analyzers/ruby/main.rb

-9
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,6 @@ def initialize(directory:, engine_config:, io:)
1818
def run
1919
flay = ::Flay.new(flay_options)
2020
flay.process(*analyzed_files)
21-
22-
flay.report(StringIO.new).each do |issue|
23-
all_locations = issue.locations
24-
25-
all_locations.each do |location|
26-
other_locations = find_other_locations(all_locations, location)
27-
io.puts "#{new_violation(issue, location, other_locations).to_json}\0"
28-
end
29-
end
3021
end
3122

3223
private

0 commit comments

Comments
 (0)