ci-syntax-tool 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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