codeclimate-fede 0.85.23
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/check +18 -0
- data/bin/codeclimate +21 -0
- data/bin/prep-release +45 -0
- data/bin/release +41 -0
- data/bin/validate-release +18 -0
- data/config/engines.yml +322 -0
- data/lib/cc/analyzer.rb +50 -0
- data/lib/cc/analyzer/bridge.rb +106 -0
- data/lib/cc/analyzer/composite_container_listener.rb +21 -0
- data/lib/cc/analyzer/container.rb +208 -0
- data/lib/cc/analyzer/container/result.rb +74 -0
- data/lib/cc/analyzer/container_listener.rb +9 -0
- data/lib/cc/analyzer/engine.rb +125 -0
- data/lib/cc/analyzer/engine_output.rb +74 -0
- data/lib/cc/analyzer/engine_output_filter.rb +36 -0
- data/lib/cc/analyzer/engine_output_overrider.rb +31 -0
- data/lib/cc/analyzer/filesystem.rb +50 -0
- data/lib/cc/analyzer/formatters.rb +21 -0
- data/lib/cc/analyzer/formatters/formatter.rb +53 -0
- data/lib/cc/analyzer/formatters/html_formatter.rb +415 -0
- data/lib/cc/analyzer/formatters/json_formatter.rb +38 -0
- data/lib/cc/analyzer/formatters/plain_text_formatter.rb +101 -0
- data/lib/cc/analyzer/formatters/spinner.rb +35 -0
- data/lib/cc/analyzer/issue.rb +69 -0
- data/lib/cc/analyzer/issue_sorter.rb +30 -0
- data/lib/cc/analyzer/issue_validations.rb +26 -0
- data/lib/cc/analyzer/issue_validations/category_validation.rb +32 -0
- data/lib/cc/analyzer/issue_validations/check_name_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/content_validation.rb +21 -0
- data/lib/cc/analyzer/issue_validations/description_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/location_format_validation.rb +72 -0
- data/lib/cc/analyzer/issue_validations/other_locations_format_validation.rb +41 -0
- data/lib/cc/analyzer/issue_validations/path_existence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/path_is_file_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/path_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +32 -0
- data/lib/cc/analyzer/issue_validations/remediation_points_validation.rb +25 -0
- data/lib/cc/analyzer/issue_validations/severity_validation.rb +39 -0
- data/lib/cc/analyzer/issue_validations/type_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/validation.rb +35 -0
- data/lib/cc/analyzer/issue_validator.rb +11 -0
- data/lib/cc/analyzer/location_description.rb +45 -0
- data/lib/cc/analyzer/logging_container_listener.rb +24 -0
- 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/mounted_path.rb +80 -0
- data/lib/cc/analyzer/raising_container_listener.rb +32 -0
- data/lib/cc/analyzer/source_buffer.rb +47 -0
- data/lib/cc/analyzer/source_extractor.rb +79 -0
- data/lib/cc/analyzer/source_fingerprint.rb +40 -0
- data/lib/cc/analyzer/statsd_container_listener.rb +51 -0
- data/lib/cc/analyzer/validator.rb +38 -0
- data/lib/cc/cli.rb +39 -0
- data/lib/cc/cli/analyze.rb +90 -0
- data/lib/cc/cli/analyze/engine_failure.rb +11 -0
- data/lib/cc/cli/command.rb +85 -0
- data/lib/cc/cli/console.rb +12 -0
- data/lib/cc/cli/engines.rb +5 -0
- data/lib/cc/cli/engines/engine_command.rb +15 -0
- data/lib/cc/cli/engines/install.rb +35 -0
- data/lib/cc/cli/engines/list.rb +18 -0
- data/lib/cc/cli/file_store.rb +42 -0
- data/lib/cc/cli/global_cache.rb +47 -0
- data/lib/cc/cli/global_config.rb +35 -0
- data/lib/cc/cli/help.rb +51 -0
- data/lib/cc/cli/output.rb +34 -0
- data/lib/cc/cli/prepare.rb +98 -0
- data/lib/cc/cli/runner.rb +75 -0
- data/lib/cc/cli/validate_config.rb +84 -0
- data/lib/cc/cli/version.rb +16 -0
- data/lib/cc/cli/version_checker.rb +107 -0
- data/lib/cc/config.rb +70 -0
- data/lib/cc/config/checks_adapter.rb +40 -0
- data/lib/cc/config/default_adapter.rb +54 -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 +93 -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/resolv.rb +39 -0
- data/lib/cc/workspace.rb +39 -0
- data/lib/cc/workspace/exclusion.rb +34 -0
- data/lib/cc/workspace/path_tree.rb +49 -0
- data/lib/cc/workspace/path_tree/dir_node.rb +67 -0
- data/lib/cc/workspace/path_tree/file_node.rb +31 -0
- metadata +277 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class ChecksAdapter
|
4
|
+
attr_reader :config
|
5
|
+
|
6
|
+
def initialize(data = {})
|
7
|
+
@config = data
|
8
|
+
|
9
|
+
return unless checks.present?
|
10
|
+
copy_qm_checks_config
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def copy_qm_checks_config
|
16
|
+
DefaultAdapter::ENGINES.keys.each do |name|
|
17
|
+
copy_checks(name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def copy_checks(engine_name)
|
22
|
+
engine = config.fetch("plugins", {}).fetch(engine_name, {})
|
23
|
+
engine["config"] ||= {}
|
24
|
+
|
25
|
+
if engine["config"].is_a?(String)
|
26
|
+
engine["config"] = {
|
27
|
+
"file" => engine["config"],
|
28
|
+
"checks" => checks,
|
29
|
+
}
|
30
|
+
elsif engine["config"].is_a?(Hash)
|
31
|
+
engine["config"]["checks"] = checks
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def checks
|
36
|
+
config["checks"]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class DefaultAdapter
|
4
|
+
# intentionally not sorted: we want them in a particular order
|
5
|
+
ENGINES = {
|
6
|
+
"structure".freeze => "stable".freeze,
|
7
|
+
"duplication".freeze => "stable".freeze,
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
EXCLUDE_PATTERNS = %w[
|
11
|
+
config/
|
12
|
+
db/
|
13
|
+
dist/
|
14
|
+
features/
|
15
|
+
**/node_modules/
|
16
|
+
script/
|
17
|
+
**/spec/
|
18
|
+
**/test/
|
19
|
+
**/tests/
|
20
|
+
Tests/
|
21
|
+
**/vendor/
|
22
|
+
**/*_test.go
|
23
|
+
**/*.d.ts
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
attr_reader :config
|
27
|
+
|
28
|
+
def initialize(data = {})
|
29
|
+
@config = data
|
30
|
+
|
31
|
+
apply_default_excludes
|
32
|
+
apply_default_engines
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def apply_default_engines
|
38
|
+
config["plugins"] ||= {}
|
39
|
+
|
40
|
+
ENGINES.each do |name, channel|
|
41
|
+
config["plugins"][name] ||= {}
|
42
|
+
unless [true, false].include?(config["plugins"][name]["enabled"])
|
43
|
+
config["plugins"][name]["enabled"] = true
|
44
|
+
end
|
45
|
+
config["plugins"][name]["channel"] ||= channel
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def apply_default_excludes
|
50
|
+
config["exclude_patterns"] ||= EXCLUDE_PATTERNS
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class Engine
|
4
|
+
DEFAULT_CHANNEL = "stable".freeze
|
5
|
+
|
6
|
+
attr_accessor :channel
|
7
|
+
attr_reader :name, :config, :exclude_patterns
|
8
|
+
attr_writer :enabled
|
9
|
+
|
10
|
+
def initialize(name, enabled: false, channel: nil, config: nil, exclude_patterns: [])
|
11
|
+
@name = name
|
12
|
+
@enabled = enabled
|
13
|
+
@channel = channel || DEFAULT_CHANNEL
|
14
|
+
@config = config || {}
|
15
|
+
@exclude_patterns = exclude_patterns
|
16
|
+
end
|
17
|
+
|
18
|
+
def enabled?
|
19
|
+
@enabled
|
20
|
+
end
|
21
|
+
|
22
|
+
def plugin?
|
23
|
+
!DefaultAdapter::ENGINES.keys.include?(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def container_label
|
27
|
+
@container_label ||= SecureRandom.uuid
|
28
|
+
end
|
29
|
+
|
30
|
+
def hash
|
31
|
+
name.hash
|
32
|
+
end
|
33
|
+
|
34
|
+
def eql?(other)
|
35
|
+
other.is_a?(self.class) && name.eql?(other.name)
|
36
|
+
end
|
37
|
+
alias_method :==, :eql?
|
38
|
+
alias_method :equal?, :eql?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class EngineSet
|
4
|
+
attr_reader :engines
|
5
|
+
|
6
|
+
def initialize(data)
|
7
|
+
@data = data
|
8
|
+
@engines = []
|
9
|
+
|
10
|
+
build_set
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :data
|
16
|
+
|
17
|
+
def build_set
|
18
|
+
DefaultAdapter::ENGINES.keys.each do |name|
|
19
|
+
if (engine = extract_engine(name))
|
20
|
+
engines << engine
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
data.each do |name, engine_data|
|
25
|
+
engines << build_engine(name, engine_data)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_engine(name)
|
30
|
+
if data[name]
|
31
|
+
engine_data = data.delete(name)
|
32
|
+
build_engine(name, engine_data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_engine(name, data)
|
37
|
+
Config::Engine.new(
|
38
|
+
name,
|
39
|
+
enabled: data.fetch("enabled", true),
|
40
|
+
channel: data["channel"],
|
41
|
+
config: data,
|
42
|
+
exclude_patterns: data.fetch("exclude_patterns", []),
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class JSONAdapter < Config
|
4
|
+
DEFAULT_PATH = ".codeclimate.json".freeze
|
5
|
+
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def self.load(path = DEFAULT_PATH)
|
9
|
+
new(::JSON.parse(File.open(path).read))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(json = {})
|
13
|
+
@config = json
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class Prepare
|
4
|
+
attr_reader :fetch
|
5
|
+
|
6
|
+
def self.from_data(data)
|
7
|
+
if data.present?
|
8
|
+
fetch = Fetch.from_data(data.fetch("fetch", []))
|
9
|
+
|
10
|
+
new(fetch: fetch)
|
11
|
+
else
|
12
|
+
new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(fetch: Fetch.new)
|
17
|
+
@fetch = fetch
|
18
|
+
end
|
19
|
+
|
20
|
+
def merge(other)
|
21
|
+
Prepare.new(fetch: fetch.merge(other.fetch))
|
22
|
+
end
|
23
|
+
|
24
|
+
class Fetch
|
25
|
+
def self.from_data(data)
|
26
|
+
new(data.map { |d| Entry.from_data(d) })
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(entries = [])
|
30
|
+
@entries = Set.new(entries)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each(&block)
|
34
|
+
entries.each(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def merge(other)
|
38
|
+
Fetch.new(each.to_a | other.each.to_a)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :entries
|
44
|
+
|
45
|
+
class Entry
|
46
|
+
attr_reader :url, :path
|
47
|
+
|
48
|
+
def self.from_data(data)
|
49
|
+
case data
|
50
|
+
when String then new(data)
|
51
|
+
when Hash then new(data.fetch("url"), data["path"])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def initialize(url, path = nil)
|
56
|
+
@url = url
|
57
|
+
@path = path || url.split("/").last
|
58
|
+
|
59
|
+
validate_path!
|
60
|
+
end
|
61
|
+
|
62
|
+
# Useful in specs
|
63
|
+
def ==(other)
|
64
|
+
other.is_a?(self.class) &&
|
65
|
+
other.url == url &&
|
66
|
+
other.path == path
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Duplicate a validation which has security implication. This should
|
72
|
+
# always be caught upstream, so raising loudly is fine.
|
73
|
+
def validate_path!
|
74
|
+
if path.blank?
|
75
|
+
raise ArgumentError, "path cannot be be blank"
|
76
|
+
end
|
77
|
+
|
78
|
+
pathname = Pathname.new(path)
|
79
|
+
|
80
|
+
if pathname.absolute?
|
81
|
+
raise ArgumentError, "path cannot be absolute: #{path}"
|
82
|
+
end
|
83
|
+
|
84
|
+
if pathname.cleanpath.to_s != pathname.to_s || path.include?("..")
|
85
|
+
raise ArgumentError, "path cannot point outside the current directory: #{path}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
class CheckValidator
|
5
|
+
include HashValidations
|
6
|
+
|
7
|
+
attr_reader :errors, :warnings
|
8
|
+
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
|
12
|
+
@errors = []
|
13
|
+
@warnings = []
|
14
|
+
|
15
|
+
validate
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :data
|
21
|
+
|
22
|
+
def validate
|
23
|
+
unless data.is_a?(Hash)
|
24
|
+
errors << "must be a hash"
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
validate_key_type("enabled", [TrueClass, FalseClass])
|
29
|
+
validate_key_type("config", Hash)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
class EngineValidator
|
5
|
+
include HashValidations
|
6
|
+
|
7
|
+
RECOGNIZED_KEYS = %w[
|
8
|
+
enabled
|
9
|
+
channel
|
10
|
+
checks
|
11
|
+
config
|
12
|
+
exclude_fingerprints
|
13
|
+
exclude_patterns
|
14
|
+
].freeze
|
15
|
+
|
16
|
+
attr_reader :errors, :warnings
|
17
|
+
|
18
|
+
def initialize(data, legacy: false)
|
19
|
+
@data = data
|
20
|
+
@legacy = legacy
|
21
|
+
|
22
|
+
@errors = []
|
23
|
+
@warnings = []
|
24
|
+
|
25
|
+
validate
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid?
|
29
|
+
errors.none?
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :data
|
35
|
+
|
36
|
+
def legacy?
|
37
|
+
@legacy
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate
|
41
|
+
validate_root
|
42
|
+
return unless data.is_a?(Hash)
|
43
|
+
|
44
|
+
validate_key_type("enabled", [TrueClass, FalseClass])
|
45
|
+
validate_key_type("channel", String)
|
46
|
+
validate_key_type("config", [String, Hash])
|
47
|
+
validate_key_type("exclude_patterns", Array)
|
48
|
+
if legacy?
|
49
|
+
validate_exclude_paths
|
50
|
+
end
|
51
|
+
|
52
|
+
validate_checks
|
53
|
+
validate_exclude_fingerprints
|
54
|
+
|
55
|
+
if legacy?
|
56
|
+
warn_unrecognized_keys(RECOGNIZED_KEYS + %w[exclude_paths])
|
57
|
+
else
|
58
|
+
warn_unrecognized_keys(RECOGNIZED_KEYS)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_root
|
63
|
+
if !data.is_a?(Hash) && ![true, false].include?(data)
|
64
|
+
errors << "section must be a boolean or a hash"
|
65
|
+
return false
|
66
|
+
end
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_checks
|
71
|
+
return unless validate_key_type("checks", Hash)
|
72
|
+
|
73
|
+
data.fetch("checks", {}).each do |_check_name, check_data|
|
74
|
+
validator = CheckValidator.new(check_data)
|
75
|
+
errors.push(*validator.errors)
|
76
|
+
warnings.push(*validator.warnings)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_exclude_paths
|
81
|
+
validate_key_type("exclude_paths", [Array, String])
|
82
|
+
if data.key?("exclude_paths")
|
83
|
+
warnings << "'exclude_paths' has been deprecated, please use 'exclude_patterns' instead"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_exclude_fingerprints
|
88
|
+
validate_key_type("exclude_fingerprints", Array)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "uri"
|
2
|
+
require "pathname"
|
3
|
+
|
4
|
+
module CC
|
5
|
+
class Config
|
6
|
+
module Validation
|
7
|
+
class FetchValidator
|
8
|
+
include HashValidations
|
9
|
+
|
10
|
+
attr_reader :errors, :warnings
|
11
|
+
|
12
|
+
def initialize(data)
|
13
|
+
@data = data
|
14
|
+
|
15
|
+
@errors = []
|
16
|
+
@warnings = []
|
17
|
+
|
18
|
+
validate
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :data
|
24
|
+
|
25
|
+
def validate
|
26
|
+
if data.is_a?(String)
|
27
|
+
validate_url(data)
|
28
|
+
elsif data.is_a?(Hash)
|
29
|
+
validate_fetch_hash
|
30
|
+
else
|
31
|
+
errors << "fetch section should be a string or a hash"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_url(url)
|
36
|
+
unless valid_url?(url)
|
37
|
+
errors << "fetch section: invalid URL '#{url}'"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_url?(url)
|
42
|
+
uri = URI.parse(url)
|
43
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
44
|
+
rescue URI::InvalidURIError
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_fetch_hash
|
49
|
+
if !data.key?("path") || !data.key?("url")
|
50
|
+
errors << "fetch section must include 'url' & 'path'"
|
51
|
+
end
|
52
|
+
|
53
|
+
validate_key_type("path", String)
|
54
|
+
validate_key_type("url", String)
|
55
|
+
|
56
|
+
validate_path(data["path"])
|
57
|
+
validate_url(data["url"])
|
58
|
+
|
59
|
+
warn_unrecognized_keys(%w[path url])
|
60
|
+
end
|
61
|
+
|
62
|
+
def validate_path(path)
|
63
|
+
if path.nil? || path.length.zero?
|
64
|
+
errors << "fetch section's 'path' cannot be empty"
|
65
|
+
else
|
66
|
+
pathname = Pathname.new(path)
|
67
|
+
if pathname.absolute?
|
68
|
+
errors << "fetch section: absolute path '#{path}' is invalid"
|
69
|
+
end
|
70
|
+
if pathname.cleanpath.to_s != pathname.to_s || path.include?("..")
|
71
|
+
errors << "fetch section: relative path elements in '#{path}' are invalid"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|