codeclimate 0.69.0 → 0.70.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/bin/prep-release +1 -1
- data/config/engines.yml +32 -323
- data/lib/cc/analyzer.rb +5 -4
- data/lib/cc/analyzer/bridge.rb +106 -0
- data/lib/cc/analyzer/composite_container_listener.rb +4 -8
- data/lib/cc/analyzer/container.rb +44 -41
- data/lib/cc/analyzer/container/result.rb +74 -0
- data/lib/cc/analyzer/container_listener.rb +2 -7
- data/lib/cc/analyzer/engine.rb +53 -45
- data/lib/cc/analyzer/engine_output.rb +40 -10
- data/lib/cc/analyzer/formatters/formatter.rb +2 -0
- data/lib/cc/analyzer/formatters/html_formatter.rb +4 -0
- data/lib/cc/analyzer/formatters/json_formatter.rb +1 -0
- data/lib/cc/analyzer/formatters/plain_text_formatter.rb +8 -1
- data/lib/cc/analyzer/issue.rb +4 -2
- data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +6 -2
- data/lib/cc/analyzer/issue_validator.rb +3 -32
- data/lib/cc/analyzer/logging_container_listener.rb +9 -7
- data/lib/cc/analyzer/measurement.rb +22 -0
- data/lib/cc/analyzer/measurement_validations.rb +16 -0
- data/lib/cc/analyzer/measurement_validations/name_validation.rb +23 -0
- data/lib/cc/analyzer/measurement_validations/type_validation.rb +15 -0
- data/lib/cc/analyzer/measurement_validations/validation.rb +27 -0
- data/lib/cc/analyzer/measurement_validations/value_validation.rb +21 -0
- data/lib/cc/analyzer/measurement_validator.rb +11 -0
- data/lib/cc/analyzer/raising_container_listener.rb +18 -18
- data/lib/cc/analyzer/statsd_container_listener.rb +22 -22
- data/lib/cc/analyzer/validator.rb +38 -0
- data/lib/cc/cli.rb +12 -12
- data/lib/cc/cli/analyze.rb +42 -60
- data/lib/cc/cli/analyze/engine_failure.rb +11 -0
- data/lib/cc/cli/command.rb +0 -10
- data/lib/cc/cli/engines.rb +0 -3
- data/lib/cc/cli/engines/engine_command.rb +2 -34
- data/lib/cc/cli/engines/install.rb +11 -17
- data/lib/cc/cli/engines/list.rb +5 -3
- data/lib/cc/cli/prepare.rb +5 -11
- data/lib/cc/cli/runner.rb +1 -2
- data/lib/cc/cli/test.rb +0 -1
- data/lib/cc/cli/validate_config.rb +49 -63
- data/lib/cc/cli/version_checker.rb +3 -3
- data/lib/cc/config.rb +70 -0
- data/lib/cc/config/checks_adapter.rb +40 -0
- data/lib/cc/config/default_adapter.rb +52 -0
- data/lib/cc/config/engine.rb +41 -0
- data/lib/cc/config/engine_set.rb +47 -0
- data/lib/cc/config/json_adapter.rb +17 -0
- data/lib/cc/config/prepare.rb +92 -0
- data/lib/cc/config/validation/check_validator.rb +34 -0
- data/lib/cc/config/validation/engine_validator.rb +89 -0
- data/lib/cc/config/validation/fetch_validator.rb +78 -0
- data/lib/cc/config/validation/file_validator.rb +112 -0
- data/lib/cc/config/validation/hash_validations.rb +52 -0
- data/lib/cc/config/validation/json.rb +31 -0
- data/lib/cc/config/validation/prepare_validator.rb +40 -0
- data/lib/cc/config/validation/yaml.rb +66 -0
- data/lib/cc/config/yaml_adapter.rb +73 -0
- data/lib/cc/engine_registry.rb +74 -0
- data/lib/cc/workspace/path_tree/dir_node.rb +1 -1
- metadata +36 -55
- data/bin/codeclimate-init +0 -6
- data/config/coffeelint/coffeelint.json +0 -129
- data/config/csslint/.csslintrc +0 -2
- data/config/eslint/.eslintignore +0 -1
- data/config/eslint/.eslintrc.yml +0 -277
- data/config/rubocop/.rubocop.yml +0 -1156
- data/lib/cc/analyzer/config.rb +0 -86
- data/lib/cc/analyzer/engine_registry.rb +0 -36
- data/lib/cc/analyzer/engines_config_builder.rb +0 -97
- data/lib/cc/analyzer/engines_runner.rb +0 -64
- data/lib/cc/cli/config.rb +0 -44
- data/lib/cc/cli/config_generator.rb +0 -108
- data/lib/cc/cli/engines/disable.rb +0 -38
- data/lib/cc/cli/engines/enable.rb +0 -41
- data/lib/cc/cli/engines/remove.rb +0 -35
- data/lib/cc/cli/init.rb +0 -117
- data/lib/cc/cli/prepare/quality.rb +0 -64
- data/lib/cc/cli/upgrade_config_generator.rb +0 -42
data/lib/cc/analyzer/config.rb
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
module CC
|
2
|
-
module Analyzer
|
3
|
-
# TODO: replace each use of this with CC::Yaml and remove it
|
4
|
-
class Config
|
5
|
-
def initialize(config_body)
|
6
|
-
@config = YAML.safe_load(config_body) || { "engines" => {} }
|
7
|
-
@config["engines"] ||= {}
|
8
|
-
|
9
|
-
expand_shorthand
|
10
|
-
end
|
11
|
-
|
12
|
-
def to_hash
|
13
|
-
@config
|
14
|
-
end
|
15
|
-
|
16
|
-
def engine_config(engine_name)
|
17
|
-
@config["engines"][engine_name] || {}
|
18
|
-
end
|
19
|
-
|
20
|
-
def engine_names
|
21
|
-
@config["engines"].keys.select { |name| engine_enabled?(name) }
|
22
|
-
end
|
23
|
-
|
24
|
-
def engine_present?(engine_name)
|
25
|
-
@config["engines"][engine_name].present?
|
26
|
-
end
|
27
|
-
|
28
|
-
def engine_enabled?(engine_name)
|
29
|
-
@config["engines"][engine_name] && @config["engines"][engine_name]["enabled"]
|
30
|
-
end
|
31
|
-
|
32
|
-
def enable_engine(engine_name)
|
33
|
-
if engine_present?(engine_name)
|
34
|
-
@config["engines"][engine_name]["enabled"] = true
|
35
|
-
else
|
36
|
-
@config["engines"][engine_name] = { "enabled" => true }
|
37
|
-
enable_default_config(engine_name) if default_config(engine_name)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def enable_default_config(engine_name)
|
42
|
-
@config["engines"][engine_name]["config"] = default_config(engine_name)
|
43
|
-
end
|
44
|
-
|
45
|
-
def exclude_paths
|
46
|
-
@config["exclude_paths"]
|
47
|
-
end
|
48
|
-
|
49
|
-
def disable_engine(engine_name)
|
50
|
-
if engine_present?(engine_name) && engine_enabled?(engine_name)
|
51
|
-
@config["engines"][engine_name]["enabled"] = false
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def remove_engine(engine_name)
|
56
|
-
if engine_present?(engine_name)
|
57
|
-
@config["engines"].delete(engine_name)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def to_yaml
|
62
|
-
@config.to_yaml
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def expand_shorthand
|
68
|
-
@config["engines"].each do |name, engine_config|
|
69
|
-
if [true, false].include?(engine_config)
|
70
|
-
@config["engines"][name] = { "enabled" => engine_config }
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def default_config(engine_name)
|
76
|
-
if (engine_config = engine_registry[engine_name])
|
77
|
-
engine_config["default_config"]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def engine_registry
|
82
|
-
@engine_registry ||= CC::Analyzer::EngineRegistry.new
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
module CC
|
2
|
-
module Analyzer
|
3
|
-
class EngineRegistry
|
4
|
-
def initialize(dev_mode = false)
|
5
|
-
@path = File.expand_path("../../../../config/engines.yml", __FILE__)
|
6
|
-
@config = YAML.safe_load(File.read(@path))
|
7
|
-
@dev_mode = dev_mode
|
8
|
-
end
|
9
|
-
|
10
|
-
def [](engine_name)
|
11
|
-
if dev_mode?
|
12
|
-
{ "channels" => { "stable" => "codeclimate/codeclimate-#{engine_name}:latest" } }
|
13
|
-
else
|
14
|
-
@config[engine_name]
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def list
|
19
|
-
@config
|
20
|
-
end
|
21
|
-
|
22
|
-
def key?(engine_name)
|
23
|
-
return true if dev_mode?
|
24
|
-
list.key?(engine_name)
|
25
|
-
end
|
26
|
-
|
27
|
-
alias_method :exists?, :key?
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def dev_mode?
|
32
|
-
@dev_mode
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
@@ -1,97 +0,0 @@
|
|
1
|
-
require "securerandom"
|
2
|
-
|
3
|
-
module CC
|
4
|
-
module Analyzer
|
5
|
-
class EnginesConfigBuilder
|
6
|
-
class RegistryAdapter < SimpleDelegator
|
7
|
-
# Calling this is guarded by Registry#key?(name) so we can assume
|
8
|
-
# metadata itself will be present. We own the YAML loaded into the
|
9
|
-
# registry, so we can also assume the "channels" key will be present. We
|
10
|
-
# can't assume it will have a key for the given channel, but the nil
|
11
|
-
# value for the returned image key will trigger the desired error
|
12
|
-
# handling.
|
13
|
-
def fetch(name, channel)
|
14
|
-
metadata = self[name]
|
15
|
-
metadata.merge("image" => metadata["channels"][channel.to_s])
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
Result = Struct.new(
|
20
|
-
:name,
|
21
|
-
:registry_entry,
|
22
|
-
:code_path,
|
23
|
-
:config,
|
24
|
-
:container_label,
|
25
|
-
)
|
26
|
-
|
27
|
-
def initialize(registry:, config:, container_label:, source_dir:, requested_paths:)
|
28
|
-
@registry = RegistryAdapter.new(registry)
|
29
|
-
@config = config
|
30
|
-
@container_label = container_label
|
31
|
-
@requested_paths = requested_paths
|
32
|
-
@source_dir = source_dir
|
33
|
-
end
|
34
|
-
|
35
|
-
def run
|
36
|
-
names_and_raw_engine_configs.map do |name, raw_engine_config|
|
37
|
-
label = @container_label || SecureRandom.uuid
|
38
|
-
engine_config = engine_config(raw_engine_config)
|
39
|
-
engine_metadata = @registry.fetch(name, raw_engine_config.channel)
|
40
|
-
Result.new(name, engine_metadata, @source_dir, engine_config, label)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
private
|
45
|
-
|
46
|
-
def engine_config(raw_engine_config)
|
47
|
-
engine_workspace = engine_workspace(raw_engine_config)
|
48
|
-
config = raw_engine_config.merge(
|
49
|
-
include_paths: engine_workspace.paths,
|
50
|
-
)
|
51
|
-
|
52
|
-
normalize_config_file(config)
|
53
|
-
|
54
|
-
config
|
55
|
-
end
|
56
|
-
|
57
|
-
def engine_workspace(raw_engine_config)
|
58
|
-
if raw_engine_config.key?("exclude_paths") && !@requested_paths.present?
|
59
|
-
base_workspace.clone.tap do |workspace|
|
60
|
-
workspace.remove(raw_engine_config["exclude_paths"])
|
61
|
-
end
|
62
|
-
else
|
63
|
-
base_workspace
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def names_and_raw_engine_configs
|
68
|
-
{}.tap do |ret|
|
69
|
-
(@config.engines || {}).each do |name, raw_engine_config|
|
70
|
-
if raw_engine_config.enabled? && @registry.key?(name)
|
71
|
-
ret[name] = raw_engine_config
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def base_workspace
|
78
|
-
@base_workspace ||= Workspace.new.tap do |workspace|
|
79
|
-
workspace.add(@requested_paths)
|
80
|
-
unless @requested_paths.present?
|
81
|
-
workspace.remove([".git"])
|
82
|
-
workspace.remove(@config.exclude_paths)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# The yaml gem turns a config file string into a hash, but engines expect
|
88
|
-
# the string. So we (for now) need to turn it into a string in that one
|
89
|
-
# scenario.
|
90
|
-
def normalize_config_file(config)
|
91
|
-
if config.fetch("config", {}).keys.size == 1 && config["config"].key?("file")
|
92
|
-
config["config"] = config["config"]["file"]
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
@@ -1,64 +0,0 @@
|
|
1
|
-
require "securerandom"
|
2
|
-
|
3
|
-
module CC
|
4
|
-
module Analyzer
|
5
|
-
class EnginesRunner
|
6
|
-
NoEnabledEngines = Class.new(StandardError)
|
7
|
-
|
8
|
-
def initialize(registry, formatter, source_dir, config, requested_paths = [], container_label = nil)
|
9
|
-
@registry = registry
|
10
|
-
@formatter = formatter
|
11
|
-
@source_dir = source_dir
|
12
|
-
@config = config
|
13
|
-
@requested_paths = requested_paths
|
14
|
-
@container_label = container_label
|
15
|
-
end
|
16
|
-
|
17
|
-
def run(container_listener = ContainerListener.new)
|
18
|
-
raise NoEnabledEngines if engines.empty?
|
19
|
-
|
20
|
-
@formatter.started
|
21
|
-
|
22
|
-
engines.each { |engine| run_engine(engine, container_listener) }
|
23
|
-
|
24
|
-
@formatter.finished
|
25
|
-
ensure
|
26
|
-
@formatter.close if @formatter.respond_to?(:close)
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
attr_reader :requested_paths
|
32
|
-
|
33
|
-
def build_engine(built_config)
|
34
|
-
Engine.new(
|
35
|
-
built_config.name,
|
36
|
-
built_config.registry_entry,
|
37
|
-
built_config.code_path,
|
38
|
-
built_config.config,
|
39
|
-
built_config.container_label,
|
40
|
-
)
|
41
|
-
end
|
42
|
-
|
43
|
-
def configs
|
44
|
-
EnginesConfigBuilder.new(
|
45
|
-
registry: @registry,
|
46
|
-
config: @config,
|
47
|
-
container_label: @container_label,
|
48
|
-
source_dir: @source_dir,
|
49
|
-
requested_paths: requested_paths,
|
50
|
-
).run
|
51
|
-
end
|
52
|
-
|
53
|
-
def engines
|
54
|
-
@engines ||= configs.map { |result| build_engine(result) }
|
55
|
-
end
|
56
|
-
|
57
|
-
def run_engine(engine, container_listener)
|
58
|
-
@formatter.engine_running(engine) do
|
59
|
-
engine.run(@formatter, container_listener)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
data/lib/cc/cli/config.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
module CC
|
2
|
-
module CLI
|
3
|
-
class Config
|
4
|
-
delegate :to_yaml, to: :config
|
5
|
-
|
6
|
-
def initialize(config = {})
|
7
|
-
@config = default_config.merge(config)
|
8
|
-
end
|
9
|
-
|
10
|
-
def add_engine(engine_name, engine_config)
|
11
|
-
config["engines"][engine_name] = { "enabled" => true }
|
12
|
-
|
13
|
-
if engine_config["default_config"].present?
|
14
|
-
config["engines"][engine_name]["config"] = engine_config["default_config"]
|
15
|
-
end
|
16
|
-
|
17
|
-
# we may not want this code in the general case.
|
18
|
-
# for now, we need it to test one of our own Maintainability engines
|
19
|
-
# which is in the 'beta' channel
|
20
|
-
if engine_config.key?("channels") && !engine_config["channels"].include?("stable")
|
21
|
-
config["engines"][engine_name]["channel"] = engine_config["channels"].first.first
|
22
|
-
end
|
23
|
-
|
24
|
-
config["ratings"]["paths"] |= engine_config["default_ratings_paths"]
|
25
|
-
end
|
26
|
-
|
27
|
-
def add_exclude_paths(paths)
|
28
|
-
config["exclude_paths"] ||= []
|
29
|
-
config["exclude_paths"] |= paths
|
30
|
-
end
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
attr_reader :config
|
35
|
-
|
36
|
-
def default_config
|
37
|
-
{
|
38
|
-
"engines" => {},
|
39
|
-
"ratings" => { "paths" => [] },
|
40
|
-
}
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,108 +0,0 @@
|
|
1
|
-
require "posix/spawn"
|
2
|
-
require "shellwords"
|
3
|
-
|
4
|
-
module CC
|
5
|
-
module CLI
|
6
|
-
class ConfigGenerator
|
7
|
-
CODECLIMATE_YAML = Command::CODECLIMATE_YAML
|
8
|
-
AUTO_EXCLUDE_PATHS = %w(config/ db/ dist/ features/ node_modules/ script/ spec/ test/ tests/ vendor/).freeze
|
9
|
-
|
10
|
-
ConfigGeneratorError = Class.new(StandardError)
|
11
|
-
|
12
|
-
def self.for(filesystem, engine_registry, upgrade_requested)
|
13
|
-
if upgrade_requested && upgrade_needed?(filesystem)
|
14
|
-
UpgradeConfigGenerator.new(filesystem, engine_registry)
|
15
|
-
else
|
16
|
-
ConfigGenerator.new(filesystem, engine_registry)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def initialize(filesystem, engine_registry)
|
21
|
-
@filesystem = filesystem
|
22
|
-
@engine_registry = engine_registry
|
23
|
-
end
|
24
|
-
|
25
|
-
def can_generate?
|
26
|
-
true
|
27
|
-
end
|
28
|
-
|
29
|
-
def eligible_engines
|
30
|
-
return @eligible_engines if @eligible_engines
|
31
|
-
|
32
|
-
engines = engine_registry.list
|
33
|
-
@eligible_engines = engines.each_with_object({}) do |(name, config), result|
|
34
|
-
if engine_eligible?(config)
|
35
|
-
result[name] = config
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def errors
|
41
|
-
[]
|
42
|
-
end
|
43
|
-
|
44
|
-
def exclude_paths
|
45
|
-
@exclude_paths ||= AUTO_EXCLUDE_PATHS.select { |path| filesystem.exist?(path) }
|
46
|
-
end
|
47
|
-
|
48
|
-
def post_generation_verb
|
49
|
-
"generated"
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
attr_reader :engine_registry, :filesystem
|
55
|
-
|
56
|
-
def self.upgrade_needed?(filesystem)
|
57
|
-
if filesystem.exist?(CODECLIMATE_YAML)
|
58
|
-
YAML.safe_load(File.read(CODECLIMATE_YAML))["languages"].present?
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def engine_eligible?(engine)
|
63
|
-
engine["channels"].keys.any? { |channel| channel == "stable" } &&
|
64
|
-
!engine["community"] &&
|
65
|
-
engine["enable_regexps"].present? &&
|
66
|
-
files_exist?(engine)
|
67
|
-
end
|
68
|
-
|
69
|
-
def files_exist?(engine)
|
70
|
-
workspace_files.any? do |path|
|
71
|
-
engine["enable_regexps"].any? { |re| Regexp.new(re).match(path) }
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def non_excluded_paths
|
76
|
-
@non_excluded_paths ||= begin
|
77
|
-
excludes = exclude_paths.map { |path| path.chomp("/") }
|
78
|
-
filesystem.ls.reject do |path|
|
79
|
-
path.starts_with?("-") || path.starts_with?(".") || excludes.include?(path)
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def workspace_files
|
85
|
-
@workspace_files ||= Dir.chdir(filesystem.root) do
|
86
|
-
if non_excluded_paths.empty?
|
87
|
-
[]
|
88
|
-
else
|
89
|
-
find_workspace_files
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def find_workspace_files
|
95
|
-
find_cmd = %w[find] + non_excluded_paths + %w[-type f -print0]
|
96
|
-
child = POSIX::Spawn::Child.new(*find_cmd)
|
97
|
-
|
98
|
-
if child.status.success?
|
99
|
-
child.out.strip.split("\0").map do |path|
|
100
|
-
path.sub(%r{^\.\/}, "")
|
101
|
-
end
|
102
|
-
else
|
103
|
-
raise ConfigGeneratorError, "Failed to find analyzable files.\nRan '#{find_cmd.shelljoin}', exited with code #{child.status.to_i} and stderr:\n'#{child.err}'"
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
require "cc/analyzer"
|
2
|
-
require "cc/cli/engines/engine_command"
|
3
|
-
|
4
|
-
module CC
|
5
|
-
module CLI
|
6
|
-
module Engines
|
7
|
-
class Disable < EngineCommand
|
8
|
-
ARGUMENT_LIST = "<engine_name>".freeze
|
9
|
-
SHORT_HELP = "Disable an engine in your codeclimate.yml.".freeze
|
10
|
-
HELP = "#{SHORT_HELP}\n" \
|
11
|
-
"\n"\
|
12
|
-
" <engine_name> Engine to disable in your codeclimate.yml".freeze
|
13
|
-
|
14
|
-
def run
|
15
|
-
require_codeclimate_yml
|
16
|
-
|
17
|
-
if !engine_exists?
|
18
|
-
say "Engine not found. Run 'codeclimate engines:list' for a list of valid engines."
|
19
|
-
elsif !engine_present_in_yaml?
|
20
|
-
say "Engine not found in .codeclimate.yml."
|
21
|
-
elsif !engine_enabled?
|
22
|
-
say "Engine already disabled."
|
23
|
-
else
|
24
|
-
disable_engine
|
25
|
-
update_yaml
|
26
|
-
say "Engine disabled."
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def disable_engine
|
33
|
-
parsed_yaml.disable_engine(engine_name)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|