omnitest-psychic 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +36 -0
- data/.travis.yml +10 -0
- data/CONTRIBUTING.md +61 -0
- data/Gemfile +23 -0
- data/LICENSE.txt +22 -0
- data/README.md +234 -0
- data/Rakefile +12 -0
- data/appveyor.yml +9 -0
- data/bin/psychic +4 -0
- data/docs/code_samples.md +92 -0
- data/docs/index.md +128 -0
- data/lib/omnitest/output_helper.rb +84 -0
- data/lib/omnitest/psychic.rb +191 -0
- data/lib/omnitest/psychic/cli.rb +171 -0
- data/lib/omnitest/psychic/code2doc.rb +75 -0
- data/lib/omnitest/psychic/code2doc/code_helper.rb +122 -0
- data/lib/omnitest/psychic/code2doc/code_segmenter.rb +170 -0
- data/lib/omnitest/psychic/code2doc/comment_styles.rb +92 -0
- data/lib/omnitest/psychic/command_template.rb +35 -0
- data/lib/omnitest/psychic/error.rb +15 -0
- data/lib/omnitest/psychic/execution/default_strategy.rb +82 -0
- data/lib/omnitest/psychic/execution/env_strategy.rb +12 -0
- data/lib/omnitest/psychic/execution/flag_strategy.rb +14 -0
- data/lib/omnitest/psychic/execution/token_strategy.rb +68 -0
- data/lib/omnitest/psychic/factories/go_factories.rb +14 -0
- data/lib/omnitest/psychic/factories/hot_read_task_factory.rb +54 -0
- data/lib/omnitest/psychic/factories/java_factories.rb +81 -0
- data/lib/omnitest/psychic/factories/powershell_factories.rb +53 -0
- data/lib/omnitest/psychic/factories/ruby_factories.rb +56 -0
- data/lib/omnitest/psychic/factories/shell_factories.rb +89 -0
- data/lib/omnitest/psychic/factories/travis_factories.rb +33 -0
- data/lib/omnitest/psychic/factory_manager.rb +46 -0
- data/lib/omnitest/psychic/file_finder.rb +69 -0
- data/lib/omnitest/psychic/hints.rb +21 -0
- data/lib/omnitest/psychic/magic_task_factory.rb +98 -0
- data/lib/omnitest/psychic/script.rb +146 -0
- data/lib/omnitest/psychic/script_factory.rb +66 -0
- data/lib/omnitest/psychic/script_factory_manager.rb +24 -0
- data/lib/omnitest/psychic/script_runner.rb +45 -0
- data/lib/omnitest/psychic/task.rb +6 -0
- data/lib/omnitest/psychic/task_factory_manager.rb +19 -0
- data/lib/omnitest/psychic/task_runner.rb +30 -0
- data/lib/omnitest/psychic/tokens.rb +51 -0
- data/lib/omnitest/psychic/version.rb +5 -0
- data/lib/omnitest/psychic/workflow.rb +27 -0
- data/lib/omnitest/shell.rb +27 -0
- data/lib/omnitest/shell/buff_shellout_executor.rb +41 -0
- data/lib/omnitest/shell/execution_result.rb +61 -0
- data/lib/omnitest/shell/mixlib_shellout_executor.rb +66 -0
- data/mkdocs.yml +6 -0
- data/omnitest-psychic.gemspec +36 -0
- data/spec/fabricators/shell_fabricator.rb +9 -0
- data/spec/omnitest/psychic/code2doc/code_helper_spec.rb +123 -0
- data/spec/omnitest/psychic/execution/default_strategy_spec.rb +17 -0
- data/spec/omnitest/psychic/execution/env_strategy_spec.rb +17 -0
- data/spec/omnitest/psychic/execution/flag_strategy_spec.rb +26 -0
- data/spec/omnitest/psychic/execution/token_strategy_spec.rb +26 -0
- data/spec/omnitest/psychic/factories/java_factories_spec.rb +35 -0
- data/spec/omnitest/psychic/factories/powershell_factories_spec.rb +59 -0
- data/spec/omnitest/psychic/factories/ruby_factories_spec.rb +91 -0
- data/spec/omnitest/psychic/factories/shell_factories_spec.rb +79 -0
- data/spec/omnitest/psychic/factories/travis_factories_spec.rb +78 -0
- data/spec/omnitest/psychic/hot_read_task_factory_spec.rb +51 -0
- data/spec/omnitest/psychic/script_factory_manager_spec.rb +57 -0
- data/spec/omnitest/psychic/script_spec.rb +55 -0
- data/spec/omnitest/psychic/shell_spec.rb +68 -0
- data/spec/omnitest/psychic/workflow_spec.rb +46 -0
- data/spec/omnitest/psychic_spec.rb +170 -0
- data/spec/spec_helper.rb +52 -0
- metadata +352 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'omnitest/core'
|
2
|
+
require 'omnitest/psychic'
|
3
|
+
|
4
|
+
# rubocop:disable Metrics/LineLength
|
5
|
+
|
6
|
+
module Omnitest
|
7
|
+
class Psychic
|
8
|
+
class BaseCLI < Omnitest::Core::CLI
|
9
|
+
include Thor::Actions
|
10
|
+
|
11
|
+
no_commands do
|
12
|
+
def psychic
|
13
|
+
@psychic ||= setup_runner
|
14
|
+
end
|
15
|
+
|
16
|
+
def setup_runner
|
17
|
+
runner_opts = { cwd: Dir.pwd, cli: shell, parameters: options.parameters }
|
18
|
+
runner_opts.merge!(Omnitest::Core::Util.symbolized_hash(options))
|
19
|
+
Omnitest::Psychic.new(runner_opts)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class List < BaseCLI
|
25
|
+
desc 'scripts', 'Lists known scripts'
|
26
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
27
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
28
|
+
def scripts
|
29
|
+
scripts = psychic.known_scripts.map do |script|
|
30
|
+
[set_color(script.name, :bold), script.source_file]
|
31
|
+
end
|
32
|
+
print_table scripts
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'tasks', 'List known tasks'
|
36
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
37
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
38
|
+
def tasks # rubocop:disable Metrics/AbcSize
|
39
|
+
psychic.known_tasks.map do |task|
|
40
|
+
task_id = set_color(task, :bold)
|
41
|
+
if options[:verbose]
|
42
|
+
details = psychic.task(task)
|
43
|
+
details = "\n#{details}".lines.join(' ') if details.lines.size > 1
|
44
|
+
say "#{task_id}: #{details}"
|
45
|
+
else
|
46
|
+
say task_id
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Show < BaseCLI
|
53
|
+
desc 'script <name>', 'Show detailed information about a script'
|
54
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
55
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
56
|
+
def script(script_name)
|
57
|
+
script = psychic.script(script_name)
|
58
|
+
say script.to_s(options[:verbose])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class CLI < BaseCLI
|
63
|
+
BUILT_IN_TASKS = %w(bootstrap unittest acceptancetest)
|
64
|
+
|
65
|
+
desc 'task <name>', 'Executes any task by name'
|
66
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
67
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
68
|
+
method_option :os, desc: "Target OS (default value is `RbConfig::CONFIG['host_os']`)"
|
69
|
+
method_option :travis, type: :boolean, desc: "Enable/disable delegation to travis-build, if it's available"
|
70
|
+
method_option :print, aliases: '-p', desc: 'Print the command (or script) instead of running it'
|
71
|
+
def task(task_alias = nil) # rubocop:disable Metrics/AbcSize
|
72
|
+
abort 'You must specify a task name, run `psychic list tasks` for a list of known tasks' unless task_alias
|
73
|
+
command = psychic.task(task_alias)
|
74
|
+
if options[:print]
|
75
|
+
say command
|
76
|
+
else
|
77
|
+
psychic.execute(command, *extra_args)
|
78
|
+
end
|
79
|
+
rescue TaskNotImplementedError => e
|
80
|
+
abort "No usable command was found for task #{task_alias}"
|
81
|
+
rescue Omnitest::Shell::ExecutionError => e
|
82
|
+
say_status :failed, task_alias, :red
|
83
|
+
say e.execution_result if e.execution_result
|
84
|
+
end
|
85
|
+
|
86
|
+
BUILT_IN_TASKS.each do |task_alias|
|
87
|
+
desc task_alias, "Executes the #{task_alias} task"
|
88
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
89
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
90
|
+
define_method(task_alias) do
|
91
|
+
task(task_alias)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
desc 'script <name>', 'Executes a script'
|
96
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
97
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
98
|
+
method_option :interactive, desc: 'Prompt for parameters?', enum: %w(always missing), lazy_default: 'missing'
|
99
|
+
method_option :parameters, desc: 'YAML file containing key/value parameters. Default: psychic-parameters.yaml'
|
100
|
+
method_option :parameter_mode, desc: 'How should the parameters be passed?', enum: %w(tokens arguments env)
|
101
|
+
method_option :os, desc: "Target OS (default value is `RbConfig::CONFIG['host_os']`)"
|
102
|
+
method_option :print, aliases: '-p', desc: 'Print the command (or script) instead of running it', lazy_default: true
|
103
|
+
def script(script_name = nil) # rubocop:disable Metrics/AbcSize
|
104
|
+
abort 'You must specify a script name, run `psychic list scripts` for a list of known scripts' unless script_name
|
105
|
+
command = psychic.script(script_name, *extra_args)
|
106
|
+
if options[:print]
|
107
|
+
say command.command(*extra_args) << "\n"
|
108
|
+
else
|
109
|
+
command.execute(*extra_args)
|
110
|
+
end
|
111
|
+
rescue ScriptNotRunnable => e
|
112
|
+
abort "No usable command was found for script #{script_name}"
|
113
|
+
rescue Omnitest::Shell::ExecutionError => e
|
114
|
+
say_status :failed, script_name, :red
|
115
|
+
say e.execution_result if e.execution_result
|
116
|
+
end
|
117
|
+
|
118
|
+
desc 'workflow [*tasks]', 'Executes multiple tasks together as a single workflow'
|
119
|
+
method_option :verbose, aliases: '-v', desc: 'Verbose: display more details'
|
120
|
+
method_option :cwd, desc: 'Working directory for detecting and running commands'
|
121
|
+
method_option :os, desc: "Target OS (default value is `RbConfig::CONFIG['host_os']`)"
|
122
|
+
method_option :travis, type: :boolean, desc: "Enable/disable delegation to travis-build, if it's available"
|
123
|
+
method_option :print, aliases: '-p', desc: 'Print the command (or script) instead of running it'
|
124
|
+
method_option :name, desc: 'A display name for the workflow'
|
125
|
+
def workflow(*tasks)
|
126
|
+
abort 'Please specify at least one task' if tasks.empty?
|
127
|
+
workflow = psychic.workflow(options[:name], options) do
|
128
|
+
tasks.each do | task_alias |
|
129
|
+
begin
|
130
|
+
task task_alias
|
131
|
+
rescue TaskNotImplementedError => e
|
132
|
+
abort "No usable command was found for task #{task_alias}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
if options[:print]
|
137
|
+
say workflow.command
|
138
|
+
else
|
139
|
+
workflow.execute({}, {}, *extra_args)
|
140
|
+
end
|
141
|
+
rescue Omnitest::Shell::ExecutionError => e
|
142
|
+
say_status :failed, options[:name], :red
|
143
|
+
say e.execution_result if e.execution_result
|
144
|
+
end
|
145
|
+
|
146
|
+
desc 'code2doc', 'Convert script to lightweight markup'
|
147
|
+
method_option :format,
|
148
|
+
enum: %w(md rst),
|
149
|
+
default: 'md',
|
150
|
+
desc: 'Target documentation format'
|
151
|
+
method_option :destination,
|
152
|
+
aliases: '-d',
|
153
|
+
default: 'docs/',
|
154
|
+
desc: 'The target directory where documentation for generated documentation.'
|
155
|
+
def code2doc(*script_names)
|
156
|
+
script_names.each do | script_name |
|
157
|
+
script = psychic.script(script_name)
|
158
|
+
target_file = File.expand_path(script.name + ".#{options[:format]}", options[:destination])
|
159
|
+
create_file(target_file, script.code2doc(options))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
desc 'list', 'List known tasks or scripts'
|
164
|
+
subcommand 'list', List
|
165
|
+
desc 'show', 'Show details about a task or script'
|
166
|
+
subcommand 'show', Show
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# rubocop:enable Metrics/LineLength
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'omnitest/psychic/code2doc/code_helper'
|
2
|
+
|
3
|
+
module Omnitest
|
4
|
+
class Psychic
|
5
|
+
module Code2Doc
|
6
|
+
module SnippetHelper
|
7
|
+
include CodeHelper
|
8
|
+
|
9
|
+
def file_snippet(file_name, opts = {})
|
10
|
+
file = expand_file(file_name)
|
11
|
+
snippet_opts = {
|
12
|
+
language: (opts[:language] || detect_language(file))
|
13
|
+
}.merge(opts)
|
14
|
+
content = file_content(file_name, snippet_opts)
|
15
|
+
snippetize(content, snippet_opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def exec_snippet(command, opts = {})
|
19
|
+
cwd = opts.delete(:cwd) || '.'
|
20
|
+
psychic = Omnitest::Psychic.new(cwd: cwd)
|
21
|
+
result = psychic.execute(command)
|
22
|
+
snippetize_output(result, opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
def snippetize_output(result, opts)
|
26
|
+
include_command = (opts.key?(:include_command) ? opts.delete(:include_command) : true)
|
27
|
+
snippet = include_command ? "$ #{result.command}\n" : ''
|
28
|
+
snippet << result.stdout
|
29
|
+
snippetize(snippet, opts)
|
30
|
+
end
|
31
|
+
|
32
|
+
def snippetize(str, opts)
|
33
|
+
language = opts.delete(:language)
|
34
|
+
snippet_opts = {
|
35
|
+
format: (opts[:format] || :markdown)
|
36
|
+
}.merge(opts)
|
37
|
+
code_block(str, language, snippet_opts).rstrip
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def file_content(file_name, opts = {})
|
43
|
+
file = expand_file(file_name)
|
44
|
+
content = file.read
|
45
|
+
after_pattern, before_pattern = [opts[:after], opts[:before]].map do | pattern |
|
46
|
+
case pattern
|
47
|
+
when nil
|
48
|
+
nil
|
49
|
+
when Regexp
|
50
|
+
pattern.source
|
51
|
+
else
|
52
|
+
Regexp.quote(pattern.to_s)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
content = content.gsub(/\A.*#{after_pattern}\s*^(.*)\Z/m, '\1') if after_pattern
|
56
|
+
content = content.gsub(/\A(.*)#{before_pattern}.*\Z/m, '\1') if before_pattern
|
57
|
+
content.rstrip
|
58
|
+
end
|
59
|
+
|
60
|
+
def detect_language(file)
|
61
|
+
file = Pathname(file)
|
62
|
+
language, _comment_style = Code2Doc::CommentStyles.infer file.extname
|
63
|
+
language
|
64
|
+
rescue CommentStyles::UnknownStyleError
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def expand_file(file_name)
|
69
|
+
file = Pathname(file_name)
|
70
|
+
file.expand_path(Omnitest.basedir) unless file.absolute?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'omnitest/psychic/code2doc/code_segmenter'
|
2
|
+
require 'rouge'
|
3
|
+
|
4
|
+
module Omnitest
|
5
|
+
class Psychic
|
6
|
+
module Code2Doc
|
7
|
+
module CodeHelper
|
8
|
+
class ReStructuredTextHelper
|
9
|
+
def self.code_block(source, language)
|
10
|
+
buffer = StringIO.new
|
11
|
+
buffer.puts ".. code-block:: #{language}"
|
12
|
+
indented_source = source.lines.map do|line|
|
13
|
+
" #{line}"
|
14
|
+
end.join("\n")
|
15
|
+
buffer.puts indented_source
|
16
|
+
buffer.string
|
17
|
+
end
|
18
|
+
end
|
19
|
+
class MarkdownHelper
|
20
|
+
def self.code_block(source, language)
|
21
|
+
buffer = StringIO.new
|
22
|
+
buffer.puts "```#{language}"
|
23
|
+
buffer.puts source
|
24
|
+
buffer.puts '```'
|
25
|
+
buffer.string
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def source
|
30
|
+
File.read absolute_source_file
|
31
|
+
end
|
32
|
+
|
33
|
+
def source?
|
34
|
+
!absolute_source_file.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def highlighted_code(formatter = 'terminal256')
|
38
|
+
language, _comment_style = Code2Doc::CommentStyles.infer source_file.extname
|
39
|
+
highlight(source, language: language, filename: absolute_source_file, formatter: formatter)
|
40
|
+
end
|
41
|
+
|
42
|
+
def code_block(source_code, language, opts = { format: :markdown })
|
43
|
+
case opts[:format].to_sym
|
44
|
+
when :rst
|
45
|
+
ReStructuredTextHelper.code_block source_code, language
|
46
|
+
when :md, :markdown
|
47
|
+
MarkdownHelper.code_block source_code, language
|
48
|
+
when :raw
|
49
|
+
source_code
|
50
|
+
else
|
51
|
+
fail ArgumentError, "Unknown format: #{opts[:format]}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Loses proper indentation on comments
|
56
|
+
def snippet_after(matcher)
|
57
|
+
segments = segmenter.segment(source)
|
58
|
+
buffer = StringIO.new
|
59
|
+
segment = segments.find do |s|
|
60
|
+
doc_segment_content = s.first.join
|
61
|
+
doc_segment_content.match matcher
|
62
|
+
end
|
63
|
+
buffer.print segment[1].join "\n" if segment # return code segment
|
64
|
+
buffer.string
|
65
|
+
end
|
66
|
+
|
67
|
+
def snippet_between(before_matcher, after_matcher)
|
68
|
+
segments = segmenter.segment(source)
|
69
|
+
start_segment = find_segment_index segments, before_matcher
|
70
|
+
end_segment = find_segment_index segments, after_matcher
|
71
|
+
buffer = StringIO.new
|
72
|
+
if start_segment && end_segment
|
73
|
+
segments[start_segment...end_segment].each do |segment|
|
74
|
+
buffer.puts @segmenter.comment(segment[0]) unless segment == segments[start_segment]
|
75
|
+
buffer.puts segment[1].join
|
76
|
+
end
|
77
|
+
end
|
78
|
+
buffer.string
|
79
|
+
end
|
80
|
+
|
81
|
+
def code2doc(options = { format: :markdown })
|
82
|
+
source_code = File.read(absolute_source_file)
|
83
|
+
segmenter_language = infer_language(source_file)
|
84
|
+
|
85
|
+
buffer = StringIO.new
|
86
|
+
segmenter_options = {
|
87
|
+
language: segmenter_language
|
88
|
+
}
|
89
|
+
segmenter = Omnitest::Psychic::Code2Doc::CodeSegmenter.new(segmenter_options)
|
90
|
+
segments = segmenter.segment source_code
|
91
|
+
segments.each do |comment, code|
|
92
|
+
comment = comment.join("\n")
|
93
|
+
code = code.join("\n")
|
94
|
+
code = code_block(code, segmenter_language, options) unless code.empty?
|
95
|
+
next if comment.empty? && code.empty?
|
96
|
+
code = "\n#{code}\n" if !comment.empty? && !code.empty? # Markdown needs separation
|
97
|
+
buffer.puts [comment, code].join("\n")
|
98
|
+
end
|
99
|
+
buffer.string
|
100
|
+
end
|
101
|
+
|
102
|
+
def infer_language(file)
|
103
|
+
language, comment_style = Psychic::Code2Doc::CommentStyles.infer File.extname(file)
|
104
|
+
segmenter_language = comment_style[:language] || language
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def segmenter
|
110
|
+
@segmenter ||= Code2Doc::CodeSegmenter.new
|
111
|
+
end
|
112
|
+
|
113
|
+
def find_segment_index(segments, matcher)
|
114
|
+
segments.find_index do |s|
|
115
|
+
doc_segment_content = s.first.join
|
116
|
+
doc_segment_content.match matcher
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'omnitest/psychic/code2doc/comment_styles'
|
2
|
+
|
3
|
+
module Omnitest
|
4
|
+
class Psychic
|
5
|
+
module Code2Doc
|
6
|
+
# This class was extracted from the [Rocco](http://rtomayko.github.com/rocco/) project
|
7
|
+
# which was in turn based on the [Docco](http://jashkenas.github.com/docco/).
|
8
|
+
class CodeSegmenter # rubocop:disable all
|
9
|
+
# Cops are disabled because the code is from Rocco
|
10
|
+
include CommentStyles
|
11
|
+
|
12
|
+
DEFAULT_OPTIONS = {
|
13
|
+
language: 'rb',
|
14
|
+
comment_chars: '#'
|
15
|
+
}
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
@options = DEFAULT_OPTIONS.merge options
|
19
|
+
@options[:comment_chars] = generate_comment_chars if @options[:comment_chars].is_a? String
|
20
|
+
end
|
21
|
+
# Internal Parsing and Highlighting
|
22
|
+
# ---------------------------------
|
23
|
+
|
24
|
+
# Parse the raw file source_code into a list of two-tuples. Each tuple has the
|
25
|
+
# form `[docs, code]` where both elements are arrays containing the
|
26
|
+
# raw lines parsed from the input file, comment characters stripped.
|
27
|
+
def segment(source_code) # rubocop:disable all
|
28
|
+
sections, docs, code = [], [], []
|
29
|
+
lines = source_code.split("\n")
|
30
|
+
|
31
|
+
# The first line is ignored if it is a shebang line. We also ignore the
|
32
|
+
# PEP 263 encoding information in python sourcefiles, and the similar ruby
|
33
|
+
# 1.9 syntax.
|
34
|
+
lines.shift if lines[0] =~ /^\#\!/
|
35
|
+
lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/ &&
|
36
|
+
%w(python rb).include?(@options[:language])
|
37
|
+
|
38
|
+
# To detect both block comments and single-line comments, we'll set
|
39
|
+
# up a tiny state machine, and loop through each line of the file.
|
40
|
+
# This requires an `in_comment_block` boolean, and a few regular
|
41
|
+
# expressions for line tests. We'll do the same for fake heredoc parsing.
|
42
|
+
in_comment_block = false
|
43
|
+
in_heredoc = false
|
44
|
+
single_line_comment, block_comment_start, block_comment_mid, block_comment_end =
|
45
|
+
nil, nil, nil, nil
|
46
|
+
unless @options[:comment_chars][:single].nil?
|
47
|
+
single_line_comment = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:single])}\\s?")
|
48
|
+
end
|
49
|
+
unless @options[:comment_chars][:multi].nil?
|
50
|
+
block_comment_start = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*$")
|
51
|
+
block_comment_end = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
52
|
+
block_comment_one_liner = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$") # rubocop:disable Metrics/LineLength
|
53
|
+
block_comment_start_with = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)$")
|
54
|
+
block_comment_end_with = Regexp.new("\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
55
|
+
if @options[:comment_chars][:multi][:middle]
|
56
|
+
block_comment_mid = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:middle])}\\s?")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
unless @options[:comment_chars][:heredoc].nil?
|
60
|
+
heredoc_start = Regexp.new("#{Regexp.escape(@options[:comment_chars][:heredoc])}(\\S+)$")
|
61
|
+
end
|
62
|
+
lines.each do |line|
|
63
|
+
# If we're currently in a comment block, check whether the line matches
|
64
|
+
# the _end_ of a comment block or the _end_ of a comment block with a
|
65
|
+
# comment.
|
66
|
+
if in_comment_block
|
67
|
+
if block_comment_end && line.match(block_comment_end)
|
68
|
+
in_comment_block = false
|
69
|
+
elsif block_comment_end_with && line.match(block_comment_end_with)
|
70
|
+
in_comment_block = false
|
71
|
+
docs << line.match(block_comment_end_with).captures.first
|
72
|
+
.sub(block_comment_mid || '', '')
|
73
|
+
else
|
74
|
+
docs << line.sub(block_comment_mid || '', '')
|
75
|
+
end
|
76
|
+
# If we're currently in a heredoc, we're looking for the end of the
|
77
|
+
# heredoc, and everything it contains is code.
|
78
|
+
elsif in_heredoc
|
79
|
+
if line.match(Regexp.new("^#{Regexp.escape(in_heredoc)}$"))
|
80
|
+
in_heredoc = false
|
81
|
+
end
|
82
|
+
code << line
|
83
|
+
# Otherwise, check whether the line starts a heredoc. If so, note the end
|
84
|
+
# pattern, and the line is code. Otherwise check whether the line matches
|
85
|
+
# the beginning of a block, or a single-line comment all on it's lonesome.
|
86
|
+
# In either case, if there's code, start a new section.
|
87
|
+
else
|
88
|
+
if heredoc_start && line.match(heredoc_start)
|
89
|
+
in_heredoc = Regexp.last_match[1]
|
90
|
+
code << line
|
91
|
+
elsif block_comment_one_liner && line.match(block_comment_one_liner)
|
92
|
+
if code.any?
|
93
|
+
sections << [docs, code]
|
94
|
+
docs, code = [], []
|
95
|
+
end
|
96
|
+
docs << line.match(block_comment_one_liner).captures.first
|
97
|
+
elsif block_comment_start && line.match(block_comment_start)
|
98
|
+
in_comment_block = true
|
99
|
+
if code.any?
|
100
|
+
sections << [docs, code]
|
101
|
+
docs, code = [], []
|
102
|
+
end
|
103
|
+
elsif block_comment_start_with && line.match(block_comment_start_with)
|
104
|
+
in_comment_block = true
|
105
|
+
if code.any?
|
106
|
+
sections << [docs, code]
|
107
|
+
docs, code = [], []
|
108
|
+
end
|
109
|
+
docs << line.match(block_comment_start_with).captures.first
|
110
|
+
elsif single_line_comment && line.match(single_line_comment)
|
111
|
+
if code.any?
|
112
|
+
sections << [docs, code]
|
113
|
+
docs, code = [], []
|
114
|
+
end
|
115
|
+
docs << line.sub(single_line_comment || '', '')
|
116
|
+
else
|
117
|
+
code << line
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
sections << [docs, code] if docs.any? || code.any?
|
122
|
+
normalize_leading_spaces(sections)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Normalizes documentation whitespace by checking for leading whitespace,
|
126
|
+
# removing it, and then removing the same amount of whitespace from each
|
127
|
+
# succeeding line. That is:
|
128
|
+
#
|
129
|
+
# def func():
|
130
|
+
# """
|
131
|
+
# Comment 1
|
132
|
+
# Comment 2
|
133
|
+
# """
|
134
|
+
# print "omg!"
|
135
|
+
#
|
136
|
+
# should yield a comment block of `Comment 1\nComment 2` and code of
|
137
|
+
# `def func():\n print "omg!"`
|
138
|
+
def normalize_leading_spaces(sections)
|
139
|
+
sections.map do |section|
|
140
|
+
if section.any? && section[0].any?
|
141
|
+
leading_space = section[0][0].match("^\s+")
|
142
|
+
if leading_space
|
143
|
+
section[0] =
|
144
|
+
section[0].map { |line| line.sub(/^#{leading_space.to_s}/, '') }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
section
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def comment(lines)
|
152
|
+
lines.map do | line |
|
153
|
+
"#{@options[:comment_chars][:single]} #{line}"
|
154
|
+
end.join "\n"
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def generate_comment_chars
|
160
|
+
@_commentchar ||=
|
161
|
+
if COMMENT_STYLES[@options[:language]]
|
162
|
+
COMMENT_STYLES[@options[:language]]
|
163
|
+
else
|
164
|
+
{ single: @options[:comment_chars], multi: nil, heredoc: nil }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end # rubocop:enable all
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|