ci-syntax-tool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/LICENSE +27 -0
- data/README.md +109 -0
- data/Rakefile +43 -0
- data/bin/ci-syntax-tool +12 -0
- data/ci-syntax-tool.gemspec +27 -0
- data/lib/ci-syntax-tool.rb +6 -0
- data/lib/ci-syntax-tool/checker.rb +63 -0
- data/lib/ci-syntax-tool/command_line.rb +229 -0
- data/lib/ci-syntax-tool/format/base.rb +50 -0
- data/lib/ci-syntax-tool/format/junit.rb +54 -0
- data/lib/ci-syntax-tool/format/progress.rb +60 -0
- data/lib/ci-syntax-tool/format_factory.rb +55 -0
- data/lib/ci-syntax-tool/language/base.rb +56 -0
- data/lib/ci-syntax-tool/language/yaml.rb +41 -0
- data/lib/ci-syntax-tool/language_factory.rb +41 -0
- data/lib/ci-syntax-tool/result.rb +134 -0
- data/lib/ci-syntax-tool/version.rb +10 -0
- data/rubocop.yml +11 -0
- data/test/features/.keep +0 -0
- data/test/features/command-line-help.feature +15 -0
- data/test/features/format-junit.feature +29 -0
- data/test/features/language-yaml.feature +34 -0
- data/test/features/pluggable-formatters.feature +42 -0
- data/test/features/pluggable-languages.feature +15 -0
- data/test/features/require-ruby.feature +38 -0
- data/test/features/step_definitions/cli_steps.rb +46 -0
- data/test/features/step_definitions/format_steps.rb +63 -0
- data/test/features/step_definitions/junit_steps.rb +57 -0
- data/test/features/step_definitions/language_steps.rb +39 -0
- data/test/features/step_definitions/require_steps.rb +38 -0
- data/test/features/support/feature_helper.rb +142 -0
- data/test/fixtures/.keep +0 -0
- data/test/fixtures/files/clean/README.md +6 -0
- data/test/fixtures/files/clean/ansiblish.yaml +12 -0
- data/test/fixtures/files/clean/kitchenish.yml +17 -0
- data/test/fixtures/files/clean/rubocopish.yaml +11 -0
- data/test/fixtures/files/error/bad-indentation.yaml +5 -0
- data/test/fixtures/files/error/missing-array-element.yaml +5 -0
- data/test/fixtures/files/error/unquoted-jinja-template.yaml +3 -0
- data/test/fixtures/files/error/very-high-yaml-version.yaml +3 -0
- data/test/fixtures/require/invalid.rb +6 -0
- data/test/fixtures/require/mock_format.rb +10 -0
- data/test/fixtures/require/second.rb +10 -0
- data/test/fixtures/require/valid.rb +10 -0
- data/test/unit/.keep +0 -0
- data/test/unit/format_factory_spec.rb +46 -0
- data/test/unit/language_factory_spec.rb +46 -0
- data/test/unit/result_spec.rb +18 -0
- data/test/unit/spec_helper.rb +31 -0
- metadata +201 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module CI
|
2
|
+
module Syntax
|
3
|
+
module Tool
|
4
|
+
module Format
|
5
|
+
# CI::Syntax::Tool::Format::Base
|
6
|
+
# Base class for syntax checkers. Sketches out the
|
7
|
+
# API you need if you want to add a format.
|
8
|
+
class Base
|
9
|
+
|
10
|
+
attr_reader :out
|
11
|
+
|
12
|
+
# Args is a hash, contents unspecified as yet.
|
13
|
+
def initialize(io, args)
|
14
|
+
@out = io
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.descendant_classes
|
18
|
+
# Fairly expensive call...
|
19
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Called once at the beginning of the check before any languages
|
23
|
+
def overall_started(_overall_result)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Called once at the beginning of the language check before any files
|
27
|
+
def lang_started(_lang_result)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Called once at the beginning of the check on a file.
|
31
|
+
def file_started(_file_result)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Called once at the end of the check on a file.
|
35
|
+
def file_finished(_file_result)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Invoked after all files are inspected, or interrupted by user.
|
39
|
+
def lang_finished(_lang_result)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Called once at the global finish
|
43
|
+
def overall_finished(_overall_result)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module CI
|
4
|
+
module Syntax
|
5
|
+
module Tool
|
6
|
+
module Format
|
7
|
+
# CI::Syntax::Tool::Format::Progress
|
8
|
+
# Prints a dot for each
|
9
|
+
# API you need if you want to add a format.
|
10
|
+
class JUnit < Format::Base
|
11
|
+
attr_reader :doc
|
12
|
+
attr_reader :root
|
13
|
+
attr_reader :testsuite # Equiv to lang
|
14
|
+
attr_reader :testcase # Equiv to file
|
15
|
+
|
16
|
+
def initialize(io, args)
|
17
|
+
super
|
18
|
+
@doc = Nokogiri::XML::Document.new()
|
19
|
+
@root = Nokogiri::XML::Element.new('testsuites', doc)
|
20
|
+
doc.add_child(root)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lang_started(lang_result)
|
24
|
+
@testsuite = Nokogiri::XML::Element.new('testsuite', doc)
|
25
|
+
testsuite['name'] = lang_result.language_name
|
26
|
+
root.add_child(testsuite)
|
27
|
+
end
|
28
|
+
|
29
|
+
def file_started(file_result)
|
30
|
+
@testcase = Nokogiri::XML::Element.new('testcase', doc)
|
31
|
+
testcase['name'] = file_result.path.gsub('/','_').gsub('.','_')
|
32
|
+
testsuite.add_child(testcase)
|
33
|
+
end
|
34
|
+
|
35
|
+
def file_finished(file_result)
|
36
|
+
file_result.issues.each do |issue|
|
37
|
+
failure = Nokogiri::XML::Element.new('failure', doc)
|
38
|
+
failure['type'] = issue.level.to_s
|
39
|
+
message = Nokogiri::XML::CDATA.new(doc, issue.cooked_message || issue.raw_message)
|
40
|
+
failure.add_child(message)
|
41
|
+
testcase.add_child(failure)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def overall_finished(overall_result)
|
46
|
+
out.write doc.to_s
|
47
|
+
out.flush
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CI
|
2
|
+
module Syntax
|
3
|
+
module Tool
|
4
|
+
module Format
|
5
|
+
# CI::Syntax::Tool::Format::Progress
|
6
|
+
# Prints a dot for each
|
7
|
+
# API you need if you want to add a format.
|
8
|
+
class Progress < Format::Base
|
9
|
+
|
10
|
+
def initialize(io, args)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def lang_started(lang_result)
|
15
|
+
out.puts "Starting syntax scan for #{lang_result.language_name}..."
|
16
|
+
end
|
17
|
+
|
18
|
+
# Called once at the end of the check on a file.
|
19
|
+
def file_finished(file_result)
|
20
|
+
if file_result.error_count > 0
|
21
|
+
out.print 'x'
|
22
|
+
elsif file_result.warning_count > 0
|
23
|
+
out.print '*'
|
24
|
+
else
|
25
|
+
out.print '.'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Invoked after all files are inspected, or interrupted by user.
|
30
|
+
def lang_finished(lang_result)
|
31
|
+
out.puts
|
32
|
+
|
33
|
+
if lang_result.warning_count > 0
|
34
|
+
out.puts 'Files with warnings:'
|
35
|
+
|
36
|
+
lang_result.warning_file_results.each do |fr|
|
37
|
+
puts ' ' + fr.path
|
38
|
+
fr.warnings.each do |w|
|
39
|
+
puts ' ' + (w.line_number.to_s || '?') + ':' + (w.character.to_s || '?') + ':' + w.raw_message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
if lang_result.error_count > 0
|
45
|
+
out.puts 'Files with errors:'
|
46
|
+
|
47
|
+
lang_result.error_file_results.each do |fr|
|
48
|
+
puts ' ' + fr.path
|
49
|
+
fr.errors.each do |e|
|
50
|
+
puts ' Line ' + (e.line_number.to_s || '?') + ': Col ' + (e.character.to_s || '?') + ':' + e.raw_message
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
# Load base class first
|
5
|
+
require_relative 'format/base'
|
6
|
+
|
7
|
+
# Load all core formats
|
8
|
+
Dir.glob(File.dirname(__FILE__) + '/format/*.rb') do |file|
|
9
|
+
require_relative file
|
10
|
+
end
|
11
|
+
|
12
|
+
module CI
|
13
|
+
module Syntax
|
14
|
+
module Tool
|
15
|
+
# CI:Syntax::Tool::FormatFactory
|
16
|
+
# Identifies and loads the Format classes, and
|
17
|
+
# creates instances as needed.
|
18
|
+
class FormatFactory
|
19
|
+
|
20
|
+
def self.all_format_classes
|
21
|
+
Format::Base.descendant_classes
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.all_format_names
|
25
|
+
class_names = all_format_classes.map(&:name)
|
26
|
+
short_names = class_names.map do |name|
|
27
|
+
name.split('::').last
|
28
|
+
end
|
29
|
+
public_names = short_names.reject {|e| e == 'MockFormat'}
|
30
|
+
public_names
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.valid_format?(proposed)
|
34
|
+
permitted_names = all_format_names << 'MockFormat'
|
35
|
+
permitted_names.include?(proposed)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.create(lang_name, io, args = {})
|
39
|
+
unless io.respond_to? :puts
|
40
|
+
if io == '-'
|
41
|
+
io = $stdout
|
42
|
+
else
|
43
|
+
# Ensure containing dir exists
|
44
|
+
FileUtils.mkdir_p File.dirname(io)
|
45
|
+
io = File.open(io, File::WRONLY | File::CREAT)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
const_get('CI::Syntax::Tool::Format::' + lang_name).new(io, args)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module CI
|
2
|
+
module Syntax
|
3
|
+
module Tool
|
4
|
+
module Language
|
5
|
+
# CI::Syntax::Tool::Language::Base
|
6
|
+
# Base class for syntax checkers. Sketches out the
|
7
|
+
# API you need if you want to add a language.
|
8
|
+
class Base
|
9
|
+
|
10
|
+
# Args is a hash, contents unspecified as yet.
|
11
|
+
def initialize(args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.descendant_classes
|
15
|
+
# Fairly expensive call...
|
16
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
17
|
+
end
|
18
|
+
|
19
|
+
# The globs that will actually be expanded.
|
20
|
+
# You probably don't need to override this.
|
21
|
+
def combined_globs
|
22
|
+
default_globs
|
23
|
+
end
|
24
|
+
|
25
|
+
# The globs you are interested in - you must
|
26
|
+
# override this to match any files. Will be
|
27
|
+
# fed to Dir.glob().
|
28
|
+
def default_globs
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Called once before any files are checked
|
33
|
+
# An opportunity to spawn a process, for example.
|
34
|
+
def check_starting(_lang_result)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Called once for each file being checked.
|
38
|
+
# file_result [Result::File] - Results object for the outcome.
|
39
|
+
# Returns: Result::File
|
40
|
+
def check_file(_file_result)
|
41
|
+
fail
|
42
|
+
end
|
43
|
+
|
44
|
+
# Called once after all files are checked
|
45
|
+
# Use for cleanup, or adding metadata to
|
46
|
+
# the result.
|
47
|
+
# result [Language::Result] - populated
|
48
|
+
# results of the run.
|
49
|
+
def check_ending(_result)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module CI
|
4
|
+
module Syntax
|
5
|
+
module Tool
|
6
|
+
module Language
|
7
|
+
# CI::Syntax::Tool::Language::Base
|
8
|
+
# Base class for syntax checkers. Sketches out the
|
9
|
+
# API you need if you want to add a language.
|
10
|
+
class YAML < Language::Base
|
11
|
+
|
12
|
+
# Args is a hash, contents unspecified as yet.
|
13
|
+
def initialize(args)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_globs
|
18
|
+
['**/*.yaml', '**/*.yml']
|
19
|
+
end
|
20
|
+
|
21
|
+
# Called once for each file being checked.
|
22
|
+
# path [String] - path to filename to check
|
23
|
+
# file_result [Result::File] - Results object for the outcome.
|
24
|
+
# Returns: Result::File
|
25
|
+
def check_file(file_result)
|
26
|
+
begin
|
27
|
+
::YAML.load(File.read(file_result.path))
|
28
|
+
rescue Psych::SyntaxError => e
|
29
|
+
file_result.add_issue(
|
30
|
+
line_number: e.line,
|
31
|
+
character: e.column,
|
32
|
+
level: :error,
|
33
|
+
raw_message: e.problem,
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
# Load base class first
|
3
|
+
require_relative 'language/base'
|
4
|
+
|
5
|
+
# Load all core languages
|
6
|
+
Dir.glob(File.dirname(__FILE__) + '/language/*.rb') do |file|
|
7
|
+
require_relative file
|
8
|
+
end
|
9
|
+
|
10
|
+
module CI
|
11
|
+
module Syntax
|
12
|
+
module Tool
|
13
|
+
# CI:Syntax::Tool::LanguageFactory
|
14
|
+
# Identifies and loads the Language classes, and
|
15
|
+
# creates instances as needed.
|
16
|
+
class LanguageFactory
|
17
|
+
|
18
|
+
def self.all_language_classes
|
19
|
+
Language::Base.descendant_classes
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.all_language_names
|
23
|
+
class_names = all_language_classes.map(&:name)
|
24
|
+
short_names = class_names.map do |name|
|
25
|
+
name.split('::').last
|
26
|
+
end
|
27
|
+
short_names
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.valid_language?(proposed)
|
31
|
+
all_language_names.include?(proposed)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.create(lang_name, args = {})
|
35
|
+
const_get('CI::Syntax::Tool::Language::' + lang_name).new(args)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module CI
|
2
|
+
module Syntax
|
3
|
+
module Tool
|
4
|
+
|
5
|
+
class Issue
|
6
|
+
attr_reader :line_number
|
7
|
+
attr_reader :character
|
8
|
+
attr_reader :raw_message
|
9
|
+
attr_reader :cooked_message
|
10
|
+
attr_reader :level
|
11
|
+
def initialize(args = {})
|
12
|
+
# Any may be nil, except for level
|
13
|
+
@line_number = args[:line_number]
|
14
|
+
@character = args[:character]
|
15
|
+
@raw_message = args[:raw_message]
|
16
|
+
@cooked_message = args[:cooked_message]
|
17
|
+
@level = args[:level] || :error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Result
|
22
|
+
|
23
|
+
class OverallResult < Result
|
24
|
+
|
25
|
+
attr_reader :language_results
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@language_results = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_language_result(lang_name)
|
32
|
+
language_results[lang_name] = LanguageResult.new(lang_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def report_failure?
|
36
|
+
error_count > 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def error_count
|
40
|
+
language_results.inject(0) do |total, (lang,result)|
|
41
|
+
total += result.error_count
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def warning_count
|
46
|
+
language_results.inject(0) do |total, (lang,result)|
|
47
|
+
total += result.warning_count
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def file_paths
|
52
|
+
files = []
|
53
|
+
language_results.each do |ln, lr|
|
54
|
+
lr.file_results.each do |fp, fr|
|
55
|
+
files << fr.path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
files
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class LanguageResult < Result
|
64
|
+
|
65
|
+
attr_reader :file_results
|
66
|
+
attr_reader :language_name
|
67
|
+
|
68
|
+
def initialize(lang_name)
|
69
|
+
@language_name = lang_name
|
70
|
+
@file_results = {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_file_result(path)
|
74
|
+
file_results[path] = FileResult.new(path)
|
75
|
+
end
|
76
|
+
|
77
|
+
def error_count
|
78
|
+
file_results.inject(0) do |total, (path,result)|
|
79
|
+
total += result.error_count
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def warning_count
|
84
|
+
file_results.inject(0) do |total, (path,result)|
|
85
|
+
total += result.warning_count
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def warning_file_results
|
90
|
+
file_results.values.select { |fr| fr.warning_count > 0 }
|
91
|
+
end
|
92
|
+
|
93
|
+
def error_file_results
|
94
|
+
file_results.values.select { |fr| fr.error_count > 0 }
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class FileResult < Result
|
100
|
+
attr_reader :path
|
101
|
+
attr_reader :issues
|
102
|
+
|
103
|
+
def initialize(path)
|
104
|
+
@path = path
|
105
|
+
@whole_file_processed = false
|
106
|
+
@issues = []
|
107
|
+
end
|
108
|
+
|
109
|
+
def add_issue(opts)
|
110
|
+
issues << Issue.new(opts)
|
111
|
+
end
|
112
|
+
|
113
|
+
def warning_count
|
114
|
+
warnings.count
|
115
|
+
end
|
116
|
+
|
117
|
+
def error_count
|
118
|
+
errors.count
|
119
|
+
end
|
120
|
+
|
121
|
+
def warnings
|
122
|
+
issues.select { |i| i.level == :warning }
|
123
|
+
end
|
124
|
+
|
125
|
+
def errors
|
126
|
+
issues.select { |i| i.level == :error }
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|