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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/LICENSE +27 -0
  4. data/README.md +109 -0
  5. data/Rakefile +43 -0
  6. data/bin/ci-syntax-tool +12 -0
  7. data/ci-syntax-tool.gemspec +27 -0
  8. data/lib/ci-syntax-tool.rb +6 -0
  9. data/lib/ci-syntax-tool/checker.rb +63 -0
  10. data/lib/ci-syntax-tool/command_line.rb +229 -0
  11. data/lib/ci-syntax-tool/format/base.rb +50 -0
  12. data/lib/ci-syntax-tool/format/junit.rb +54 -0
  13. data/lib/ci-syntax-tool/format/progress.rb +60 -0
  14. data/lib/ci-syntax-tool/format_factory.rb +55 -0
  15. data/lib/ci-syntax-tool/language/base.rb +56 -0
  16. data/lib/ci-syntax-tool/language/yaml.rb +41 -0
  17. data/lib/ci-syntax-tool/language_factory.rb +41 -0
  18. data/lib/ci-syntax-tool/result.rb +134 -0
  19. data/lib/ci-syntax-tool/version.rb +10 -0
  20. data/rubocop.yml +11 -0
  21. data/test/features/.keep +0 -0
  22. data/test/features/command-line-help.feature +15 -0
  23. data/test/features/format-junit.feature +29 -0
  24. data/test/features/language-yaml.feature +34 -0
  25. data/test/features/pluggable-formatters.feature +42 -0
  26. data/test/features/pluggable-languages.feature +15 -0
  27. data/test/features/require-ruby.feature +38 -0
  28. data/test/features/step_definitions/cli_steps.rb +46 -0
  29. data/test/features/step_definitions/format_steps.rb +63 -0
  30. data/test/features/step_definitions/junit_steps.rb +57 -0
  31. data/test/features/step_definitions/language_steps.rb +39 -0
  32. data/test/features/step_definitions/require_steps.rb +38 -0
  33. data/test/features/support/feature_helper.rb +142 -0
  34. data/test/fixtures/.keep +0 -0
  35. data/test/fixtures/files/clean/README.md +6 -0
  36. data/test/fixtures/files/clean/ansiblish.yaml +12 -0
  37. data/test/fixtures/files/clean/kitchenish.yml +17 -0
  38. data/test/fixtures/files/clean/rubocopish.yaml +11 -0
  39. data/test/fixtures/files/error/bad-indentation.yaml +5 -0
  40. data/test/fixtures/files/error/missing-array-element.yaml +5 -0
  41. data/test/fixtures/files/error/unquoted-jinja-template.yaml +3 -0
  42. data/test/fixtures/files/error/very-high-yaml-version.yaml +3 -0
  43. data/test/fixtures/require/invalid.rb +6 -0
  44. data/test/fixtures/require/mock_format.rb +10 -0
  45. data/test/fixtures/require/second.rb +10 -0
  46. data/test/fixtures/require/valid.rb +10 -0
  47. data/test/unit/.keep +0 -0
  48. data/test/unit/format_factory_spec.rb +46 -0
  49. data/test/unit/language_factory_spec.rb +46 -0
  50. data/test/unit/result_spec.rb +18 -0
  51. data/test/unit/spec_helper.rb +31 -0
  52. 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