pdd 0.20.7 → 0.21.1
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 +4 -4
- data/.github/workflows/codecov.yml +20 -0
- data/.github/workflows/rake.yml +24 -0
- data/.gitignore +5 -5
- data/.overcommit.yml +96 -0
- data/.pdd +3 -1
- data/.rubocop.yml +5 -6
- data/.rultor.yml +8 -15
- data/.simplecov +6 -6
- data/CITATION.cff +25 -0
- data/Gemfile +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +46 -16
- data/Rakefile +7 -1
- data/assets/puzzles.xsd +1 -1
- data/assets/puzzles.xsl +1 -1
- data/bin/pdd +22 -21
- data/features/catches_broken_puzzles.feature +2 -20
- data/features/cli.feature +1 -1
- data/features/html_output.feature +1 -1
- data/features/parsing.feature +31 -1
- data/features/rake.feature +14 -12
- data/features/step_definitions/steps.rb +7 -10
- data/features/support/env.rb +1 -1
- data/features/uses_config.feature +1 -1
- data/lib/pdd/puzzle.rb +1 -1
- data/lib/pdd/rake_task.rb +11 -12
- data/lib/pdd/rule/duplicates.rb +2 -1
- data/lib/pdd/rule/estimates.rb +1 -1
- data/lib/pdd/rule/roles.rb +2 -1
- data/lib/pdd/rule/text.rb +2 -1
- data/lib/pdd/source.rb +58 -54
- data/lib/pdd/sources.rb +40 -32
- data/lib/pdd/version.rb +3 -3
- data/lib/pdd.rb +16 -16
- data/pdd.gemspec +7 -4
- data/test/test__helper.rb +2 -2
- data/test/test_duplicates.rb +2 -2
- data/test/test_estimates.rb +2 -2
- data/test/test_pdd.rb +3 -2
- data/test/test_rake_task.rb +7 -4
- data/test/test_roles.rb +2 -2
- data/test/test_source.rb +74 -17
- data/test/test_source_todo.rb +15 -21
- data/test/test_sources.rb +7 -6
- data/test/test_text.rb +2 -2
- data/utils/glob.rb +65 -0
- metadata +36 -5
- data/.travis.yml +0 -13
- data/appveyor.yml +0 -21
data/features/parsing.feature
CHANGED
@@ -25,6 +25,32 @@ Feature: Parsing
|
|
25
25
|
And XML matches "//puzzle[role='DEV']"
|
26
26
|
And XML matches "//puzzle[estimate='0']"
|
27
27
|
|
28
|
+
Scenario: Simple puzzle within comment block
|
29
|
+
Given I have a "test/a/b/Sample.java" file with content:
|
30
|
+
"""
|
31
|
+
public class Main {
|
32
|
+
/**
|
33
|
+
* Some other documentation
|
34
|
+
* text that is not relevant to
|
35
|
+
* the puzzle below.
|
36
|
+
* @todo #13 This puzzle has a correct format
|
37
|
+
* It doesn't start with a space on
|
38
|
+
* the second and the third lines
|
39
|
+
*/
|
40
|
+
public void main(String[] args) {
|
41
|
+
// later
|
42
|
+
}
|
43
|
+
}
|
44
|
+
"""
|
45
|
+
When I run pdd
|
46
|
+
Then XML matches "/puzzles[count(puzzle)=1]"
|
47
|
+
And XML matches "//puzzle[file='test/a/b/Sample.java']"
|
48
|
+
And XML matches "//puzzle[ticket='13']"
|
49
|
+
And XML matches "//puzzle[lines='6-8']"
|
50
|
+
And XML matches "//puzzle[starts-with(body,'This')]"
|
51
|
+
And XML matches "//puzzle[role='DEV']"
|
52
|
+
And XML matches "//puzzle[estimate='0']"
|
53
|
+
|
28
54
|
Scenario: Multiple puzzles in one file
|
29
55
|
Given I have a "test/a/b/c/Sample.java" file with content:
|
30
56
|
"""
|
@@ -34,6 +60,9 @@ Feature: Parsing
|
|
34
60
|
* @todo #ABC-67:15min And this one ever later
|
35
61
|
* @todo #F-78-3:2h/DEV This is for a developer
|
36
62
|
* who will join us later
|
63
|
+
* @todo #44 This puzzle has a correct format
|
64
|
+
* even though it doesn't start with a space on
|
65
|
+
* the second and the third lines
|
37
66
|
*/
|
38
67
|
public void main(String[] args) {
|
39
68
|
// later
|
@@ -41,10 +70,11 @@ Feature: Parsing
|
|
41
70
|
}
|
42
71
|
"""
|
43
72
|
When I run pdd
|
44
|
-
Then XML matches "/puzzles[count(puzzle)=
|
73
|
+
Then XML matches "/puzzles[count(puzzle)=4]"
|
45
74
|
And XML matches "//puzzle[ticket='13' and lines='3-3']"
|
46
75
|
And XML matches "//puzzle[ticket='13' and body='This one later']"
|
47
76
|
And XML matches "//puzzle[ticket='ABC-67' and lines='4-4']"
|
48
77
|
And XML matches "//puzzle[ticket='F-78-3' and lines='5-6']"
|
49
78
|
And XML matches "//puzzle[ticket='ABC-67' and estimate='15']"
|
50
79
|
And XML matches "//puzzle[ticket='F-78-3' and estimate='120']"
|
80
|
+
And XML matches "//puzzle[ticket='44' and lines='7-9']"
|
data/features/rake.feature
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
Feature: Rake Task
|
2
|
-
|
2
|
+
As a source code writer I want to be able to
|
3
|
+
run PDD from Rakefile
|
4
|
+
Scenario: PDD can be used in Rakefile
|
3
5
|
Given It is Unix
|
4
|
-
And
|
6
|
+
And I have a "Rakefile" file with content:
|
5
7
|
"""
|
6
8
|
require 'pdd/rake_task'
|
7
|
-
PDD::RakeTask.new
|
9
|
+
PDD::RakeTask.new(:pdd) do |task|
|
10
|
+
task.includes = ['a.txt']
|
11
|
+
end
|
8
12
|
"""
|
9
|
-
|
10
|
-
"""bash
|
11
|
-
rake pdd
|
13
|
+
And I have a "a.txt" file with content:
|
12
14
|
"""
|
13
|
-
|
14
|
-
And the stderr should contain:
|
15
|
+
\x40todo #55 hello!
|
15
16
|
"""
|
16
|
-
|
17
|
-
""
|
18
|
-
|
19
|
-
|
17
|
+
|
18
|
+
When I run bash with "rake pdd"
|
19
|
+
Then Exit code is zero
|
20
|
+
|
21
|
+
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -64,9 +64,9 @@ When(/^I run pdd it fails with "([^"]*)"$/) do |txt|
|
|
64
64
|
begin
|
65
65
|
PDD::Base.new(@opts).xml
|
66
66
|
passed = true
|
67
|
-
rescue PDD::Error =>
|
68
|
-
unless
|
69
|
-
raise "PDD failed but exception doesn't contain \"#{txt}\": #{
|
67
|
+
rescue PDD::Error => e
|
68
|
+
unless e.message.include?(txt)
|
69
|
+
raise "PDD failed but exception doesn't contain \"#{txt}\": #{e.message}"
|
70
70
|
end
|
71
71
|
end
|
72
72
|
raise "PDD didn't fail" if passed
|
@@ -79,9 +79,7 @@ When(%r{^I run bin/pdd with "([^"]*)"$}) do |arg|
|
|
79
79
|
end
|
80
80
|
|
81
81
|
Then(/^Stdout contains "([^"]*)"$/) do |txt|
|
82
|
-
unless @stdout.include?(txt)
|
83
|
-
raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}"
|
84
|
-
end
|
82
|
+
raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
|
85
83
|
end
|
86
84
|
|
87
85
|
Then(/^Stdout is empty$/) do
|
@@ -90,11 +88,10 @@ end
|
|
90
88
|
|
91
89
|
Then(/^XML file "([^"]+)" matches "([^"]+)"$/) do |file, xpath|
|
92
90
|
raise "File #{file} doesn't exit" unless File.exist?(file)
|
91
|
+
|
93
92
|
xml = Nokogiri::XML.parse(File.read(file))
|
94
93
|
xml.remove_namespaces!
|
95
|
-
if xml.xpath(xpath).empty?
|
96
|
-
raise "XML file #{file} doesn't match \"#{xpath}\":\n#{xml}"
|
97
|
-
end
|
94
|
+
raise "XML file #{file} doesn't match \"#{xpath}\":\n#{xml}" if xml.xpath(xpath).empty?
|
98
95
|
end
|
99
96
|
|
100
97
|
Then(/^Exit code is zero$/) do
|
data/features/support/env.rb
CHANGED
data/lib/pdd/puzzle.rb
CHANGED
data/lib/pdd/rake_task.rb
CHANGED
@@ -1,30 +1,29 @@
|
|
1
1
|
require 'rake'
|
2
2
|
require 'rake/tasklib'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'slop'
|
7
|
+
require 'pdd'
|
3
8
|
|
4
9
|
# PDD Rake task
|
5
10
|
module PDD
|
6
11
|
# Rake task
|
7
12
|
class RakeTask < Rake::TaskLib
|
8
|
-
attr_accessor :name
|
13
|
+
attr_accessor :name, :fail_on_error, :includes, :license, :quiet
|
14
|
+
|
9
15
|
def initialize(*args, &task_block)
|
10
|
-
# @todo #125:30m Needs to have more parameters to run this task.
|
11
|
-
# For now, we just have a single parameter - the name.
|
12
|
-
# Needs more parameters like the xcop rake task has.
|
13
16
|
@name = args.shift || :pdd
|
17
|
+
@includes = []
|
18
|
+
@excludes = []
|
19
|
+
@license = nil
|
20
|
+
@quiet = false
|
14
21
|
desc 'Run PDD' unless ::Rake.application.last_description
|
15
22
|
task(name, *args) do |_, task_args|
|
16
23
|
RakeFileUtils.send(:verbose, true) do
|
17
24
|
yield(*[self, task_args].slice(0, task_block.arity)) if block_given?
|
18
|
-
run
|
19
25
|
end
|
20
26
|
end
|
21
27
|
end
|
22
|
-
|
23
|
-
def run
|
24
|
-
# @todo #125:30m need to implement this method.
|
25
|
-
# For now, it's just a task,
|
26
|
-
# that accepts one parameter and throws a system error.
|
27
|
-
abort('NOT IMPLEMENTED')
|
28
|
-
end
|
29
28
|
end
|
30
29
|
end
|
data/lib/pdd/rule/duplicates.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -35,6 +35,7 @@ module PDD
|
|
35
35
|
.group_by { |p| p.xpath('body/text()').to_s }
|
36
36
|
.map do |_, puzzles|
|
37
37
|
next nil if puzzles.count <= @max
|
38
|
+
|
38
39
|
"there are #{puzzles.count} duplicate(s) of the same puzzle: " +
|
39
40
|
puzzles.map do |p|
|
40
41
|
"#{p.xpath('file/text()')}:#{p.xpath('lines/text()')}"
|
data/lib/pdd/rule/estimates.rb
CHANGED
data/lib/pdd/rule/roles.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -34,6 +34,7 @@ module PDD
|
|
34
34
|
@xml.xpath('//puzzle').map do |p|
|
35
35
|
role = p.xpath('role/text()').to_s
|
36
36
|
next nil if @roles.include?(role)
|
37
|
+
|
37
38
|
"puzzle #{p.xpath('file/text()')}:#{p.xpath('lines/text()')}" +
|
38
39
|
if role.empty?
|
39
40
|
" doesn't define any role"\
|
data/lib/pdd/rule/text.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -34,6 +34,7 @@ module PDD
|
|
34
34
|
@xml.xpath('//puzzle').map do |p|
|
35
35
|
words = p.xpath('body/text()').to_s.split.size
|
36
36
|
next nil if words >= @min
|
37
|
+
|
37
38
|
"Puzzle #{p.xpath('file/text()')}:#{p.xpath('lines/text()')}"\
|
38
39
|
" has a very short description of just #{words} words while"\
|
39
40
|
" a minimum of #{@min} is required"
|
data/lib/pdd/source.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -37,10 +37,31 @@ module PDD
|
|
37
37
|
@path = path
|
38
38
|
end
|
39
39
|
|
40
|
-
def match_markers(
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
def match_markers(l)
|
41
|
+
if l.downcase.include? 'todo'
|
42
|
+
/[^\s]\x40todo/.match(l) do |_|
|
43
|
+
raise Error, get_no_leading_space_error("\x40todo")
|
44
|
+
end
|
45
|
+
/\x40todo(?!\s+#)/.match(l) do |_|
|
46
|
+
raise Error, get_no_puzzle_marker_error("\x40todo")
|
47
|
+
end
|
48
|
+
/\x40todo\s+#\s/.match(l) do |_|
|
49
|
+
raise Error, get_space_after_hash_error("\x40todo")
|
50
|
+
end
|
51
|
+
/[^\s]TODO:?/.match(l) do |_|
|
52
|
+
raise Error, get_no_leading_space_error('TODO')
|
53
|
+
end
|
54
|
+
/TODO(?!:?\s+#)/.match(l) do |_|
|
55
|
+
raise Error, get_no_puzzle_marker_error('TODO')
|
56
|
+
end
|
57
|
+
/TODO:?\s+#\s/.match(l) do |_|
|
58
|
+
raise Error, get_space_after_hash_error('TODO')
|
59
|
+
end
|
60
|
+
a = [%r{(.*(?:^|\s))(?:\x40todo|TODO:|TODO)\s+#([\w\-.:/]+)\s+(.+)}.match(l)]
|
61
|
+
a.compact
|
62
|
+
else
|
63
|
+
[]
|
64
|
+
end
|
44
65
|
end
|
45
66
|
|
46
67
|
# Fetch all puzzles.
|
@@ -50,7 +71,6 @@ module PDD
|
|
50
71
|
lines = File.readlines(@file, encoding: 'UTF-8')
|
51
72
|
lines.each_with_index do |line, idx|
|
52
73
|
begin
|
53
|
-
check_rules(line)
|
54
74
|
match_markers(line).each do |m|
|
55
75
|
puzzles << puzzle(lines.drop(idx + 1), m, idx)
|
56
76
|
end
|
@@ -82,31 +102,11 @@ after the hash sign, it should not be there, \
|
|
82
102
|
see https://github.com/cqfn/pdd#how-to-format"
|
83
103
|
end
|
84
104
|
|
85
|
-
def check_rules(line)
|
86
|
-
/[^\s]\x40todo/.match(line) do |_|
|
87
|
-
raise Error, get_no_leading_space_error("\x40todo")
|
88
|
-
end
|
89
|
-
/\x40todo(?!\s+#)/.match(line) do |_|
|
90
|
-
raise Error, get_no_puzzle_marker_error("\x40todo")
|
91
|
-
end
|
92
|
-
/\x40todo\s+#\s/.match(line) do |_|
|
93
|
-
raise Error, get_space_after_hash_error("\x40todo")
|
94
|
-
end
|
95
|
-
/[^\s]TODO:?/.match(line) do |_|
|
96
|
-
raise Error, get_no_leading_space_error('TODO')
|
97
|
-
end
|
98
|
-
/TODO(?!:?\s+#)/.match(line) do |_|
|
99
|
-
raise Error, get_no_puzzle_marker_error('TODO')
|
100
|
-
end
|
101
|
-
/TODO:?\s+#\s/.match(line) do |_|
|
102
|
-
raise Error, get_space_after_hash_error('TODO')
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
105
|
# Fetch puzzle
|
107
106
|
def puzzle(lines, match, idx)
|
108
|
-
|
109
|
-
|
107
|
+
col_idx = match[0].length - match[0].lstrip.length
|
108
|
+
tail = tail(lines, match[1], col_idx)
|
109
|
+
body = "#{match[3]} #{tail.join(' ')}".gsub(/\s+/, ' ').strip
|
110
110
|
body = body.chomp('*/-->').strip
|
111
111
|
marker = marker(match[2])
|
112
112
|
Puzzle.new(
|
@@ -121,7 +121,7 @@ see https://github.com/cqfn/pdd#how-to-format"
|
|
121
121
|
|
122
122
|
# Parse a marker.
|
123
123
|
def marker(text)
|
124
|
-
re = %r{([\w
|
124
|
+
re = %r{([\w\-.]+)(?::(\d+)(?:(m|h)[a-z]*)?)?(?:/([A-Z]+))?}
|
125
125
|
match = re.match(text)
|
126
126
|
if match.nil?
|
127
127
|
raise "Invalid puzzle marker \"#{text}\", most probably formatted \
|
@@ -143,16 +143,17 @@ against the rules explained here: https://github.com/cqfn/pdd#how-to-format"
|
|
143
143
|
|
144
144
|
# Fetch puzzle tail (all lines after the first one)
|
145
145
|
def tail(lines, prefix, start)
|
146
|
+
prefix = prefix.rstrip
|
147
|
+
prefix = " #{' ' * start}" if prefix.empty? # fallback to space indentation
|
146
148
|
lines
|
147
149
|
.take_while { |t| match_markers(t).none? && t.start_with?(prefix) }
|
148
|
-
.
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
raise Error, "Space expected at #{start + i + 2}:#{prefix.length}; \
|
153
|
-
make sure all lines in the puzzle body have a single leading space."
|
150
|
+
.take_while do |t|
|
151
|
+
# account for carriage return in line endings
|
152
|
+
t_len = t.length - 1
|
153
|
+
t_len <= prefix.length || t_len > prefix.length + 2
|
154
154
|
end
|
155
|
-
.map { |t| t[
|
155
|
+
.map { |t| t[prefix.length, t.length] }
|
156
|
+
.map { |t| t.start_with?(' ') ? t[1, t.length] : t }
|
156
157
|
end
|
157
158
|
|
158
159
|
# @todo #75:30min Let's make it possible to fetch Subversion data
|
@@ -167,21 +168,22 @@ make sure all lines in the puzzle body have a single leading space."
|
|
167
168
|
if `#{git} rev-parse --is-inside-work-tree 2>/dev/null`.strip == 'true'
|
168
169
|
cmd = "#{git} blame -L #{pos},#{pos} --porcelain #{name}"
|
169
170
|
add_github_login(Hash[
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
171
|
+
`#{cmd}`.split("\n").map do |line|
|
172
|
+
case line
|
173
|
+
when /^author /
|
174
|
+
[:author, line.sub(/^author /, '')]
|
175
|
+
when /^author-mail [^@]+@[^.]+\..+/
|
176
|
+
[:email, line.sub(/^author-mail <(.+)>$/, '\1')]
|
177
|
+
when /^author-time /
|
178
|
+
[
|
179
|
+
:time,
|
180
|
+
Time.at(
|
181
|
+
line.sub(/^author-time ([0-9]+)$/, '\1').to_i
|
182
|
+
).utc.iso8601
|
183
|
+
]
|
184
|
+
end
|
185
|
+
end.compact
|
186
|
+
])
|
185
187
|
else
|
186
188
|
{}
|
187
189
|
end
|
@@ -207,12 +209,14 @@ make sure all lines in the puzzle body have a single leading space."
|
|
207
209
|
email, author = info.values_at(:email, :author)
|
208
210
|
# if email is not defined, changes have not been committed
|
209
211
|
return if email.nil?
|
212
|
+
|
210
213
|
base_uri = 'https://api.github.com/search/users?per_page=1'
|
211
214
|
query = base_uri + "&q=#{email}+in:email"
|
212
215
|
json = get_json query
|
213
216
|
# find user by name instead since users can make github email private
|
214
217
|
unless json['total_count'].positive?
|
215
218
|
return if author.nil?
|
219
|
+
|
216
220
|
query = base_uri + "&q=#{author}+in:fullname"
|
217
221
|
json = get_json query
|
218
222
|
end
|
@@ -240,8 +244,8 @@ make sure all lines in the puzzle body have a single leading space."
|
|
240
244
|
# Fetch all puzzles.
|
241
245
|
def puzzles
|
242
246
|
@source.puzzles
|
243
|
-
rescue Error =>
|
244
|
-
raise Error, "#{@file}; #{
|
247
|
+
rescue Error => e
|
248
|
+
raise Error, "#{@file}; #{e.message}"
|
245
249
|
end
|
246
250
|
end
|
247
251
|
end
|
data/lib/pdd/sources.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -18,71 +18,79 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
19
|
# SOFTWARE.
|
20
20
|
|
21
|
-
require '
|
21
|
+
require 'rainbow'
|
22
22
|
require 'English'
|
23
|
+
require 'filemagic'
|
23
24
|
require_relative 'source'
|
25
|
+
require_relative '../../utils/glob'
|
24
26
|
|
25
27
|
module PDD
|
26
28
|
# Code base abstraction
|
27
29
|
class Sources
|
28
30
|
# Ctor.
|
29
31
|
# +dir+:: Directory with source code files
|
30
|
-
def initialize(dir
|
32
|
+
def initialize(dir)
|
31
33
|
@dir = File.absolute_path(dir)
|
32
|
-
@exclude =
|
33
|
-
@include =
|
34
|
+
@exclude = ['.git/**/*']
|
35
|
+
@include = []
|
34
36
|
end
|
35
37
|
|
36
38
|
# Fetch all sources.
|
37
39
|
def fetch
|
40
|
+
exclude_paths = @exclude.map do |ptn|
|
41
|
+
Glob.new(File.join(@dir, ptn)).to_regexp
|
42
|
+
end
|
38
43
|
files = Dir.glob(
|
39
44
|
File.join(@dir, '**/*'), File::FNM_DOTMATCH
|
40
|
-
).reject
|
41
|
-
|
42
|
-
@include.each do |ptn|
|
43
|
-
Dir.glob(File.join(@dir, ptn), File::FNM_DOTMATCH) do |f|
|
44
|
-
files.keep_if { |i| i != f }
|
45
|
-
included += 1
|
46
|
-
end
|
45
|
+
).reject do |f|
|
46
|
+
File.directory?(f) || exclude_paths.any? { |ptn| f.match(ptn) }
|
47
47
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
files.delete_if { |i| i == f }
|
53
|
-
excluded += 1
|
54
|
-
end
|
55
|
-
end
|
56
|
-
PDD.log.info "#{files.size} file(s) found, #{excluded} excluded"
|
48
|
+
files += Dir.glob(
|
49
|
+
@include.map { |ptn| File.join(@dir, ptn) }
|
50
|
+
).reject { |f| File.directory?(f) }
|
51
|
+
files = files.uniq # remove duplicates
|
57
52
|
files.reject { |f| binary?(f) }.map do |file|
|
58
53
|
path = file[@dir.length + 1, file.length]
|
59
54
|
VerboseSource.new(path, Source.new(file, path))
|
60
55
|
end
|
61
56
|
end
|
62
57
|
|
63
|
-
def exclude(
|
64
|
-
|
58
|
+
def exclude(paths)
|
59
|
+
paths = paths.nil? ? [] : paths
|
60
|
+
paths = paths.is_a?(Array) ? paths : [paths]
|
61
|
+
@exclude.push(*paths)
|
62
|
+
paths&.each do |path|
|
63
|
+
PDD.log.info "#{Rainbow('Excluding').orange} #{path}"
|
64
|
+
end
|
65
|
+
self
|
65
66
|
end
|
66
67
|
|
67
|
-
def include(
|
68
|
-
|
68
|
+
def include(paths)
|
69
|
+
paths = paths.nil? ? [] : paths
|
70
|
+
paths = paths.is_a?(Array) ? paths : [paths]
|
71
|
+
@include.push(*paths)
|
72
|
+
paths&.each do |path|
|
73
|
+
PDD.log.info "#{Rainbow('Including').blue} #{path}"
|
74
|
+
end
|
75
|
+
self
|
69
76
|
end
|
70
77
|
|
71
78
|
private
|
72
79
|
|
73
|
-
# @todo #98:30min Change the implementation of this method
|
74
|
-
# to also work in Windows machines. Investigate the possibility
|
75
|
-
# of use a gem for this. After that, remove the skip of the test
|
76
|
-
# `test_ignores_binary_files` in `test_sources.rb`.
|
77
80
|
def binary?(file)
|
78
|
-
|
79
|
-
`grep -qI '.' #{Shellwords.escape(file)}`
|
80
|
-
if $CHILD_STATUS.success?
|
81
|
+
if text_file?(file)
|
81
82
|
false
|
82
83
|
else
|
83
84
|
PDD.log.info "#{file} is a binary file (#{File.size(file)} bytes)"
|
84
85
|
true
|
85
86
|
end
|
86
87
|
end
|
88
|
+
|
89
|
+
def text_file?(file)
|
90
|
+
fm = FileMagic.new(FileMagic::MAGIC_MIME)
|
91
|
+
fm.file(file) =~ %r{^text/}
|
92
|
+
ensure
|
93
|
+
fm.close
|
94
|
+
end
|
87
95
|
end
|
88
96
|
end
|
data/lib/pdd/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -20,8 +20,8 @@
|
|
20
20
|
|
21
21
|
# PDD main module.
|
22
22
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
23
|
-
# Copyright:: Copyright (c) 2014-
|
23
|
+
# Copyright:: Copyright (c) 2014-2022 Yegor Bugayenko
|
24
24
|
# License:: MIT
|
25
25
|
module PDD
|
26
|
-
VERSION = '0.
|
26
|
+
VERSION = '0.21.1'.freeze
|
27
27
|
end
|
data/lib/pdd.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -30,7 +30,7 @@ require_relative 'pdd/rule/roles'
|
|
30
30
|
|
31
31
|
# PDD main module.
|
32
32
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
33
|
-
# Copyright:: Copyright (c) 2014-
|
33
|
+
# Copyright:: Copyright (c) 2014-2022 Yegor Bugayenko
|
34
34
|
# License:: MIT
|
35
35
|
module PDD
|
36
36
|
# If it breaks.
|
@@ -52,11 +52,12 @@ module PDD
|
|
52
52
|
# Get logger.
|
53
53
|
def self.log
|
54
54
|
unless defined?(@logger)
|
55
|
-
@logger = Logger.new(
|
55
|
+
@logger = Logger.new($stdout)
|
56
56
|
@logger.formatter = proc { |severity, _, _, msg|
|
57
|
-
|
57
|
+
case severity
|
58
|
+
when 'ERROR'
|
58
59
|
"#{Rainbow(severity).red}: #{msg}\n"
|
59
|
-
|
60
|
+
when 'WARN'
|
60
61
|
"#{Rainbow(severity).orange}: #{msg}\n"
|
61
62
|
else
|
62
63
|
"#{msg}\n"
|
@@ -87,20 +88,15 @@ module PDD
|
|
87
88
|
|
88
89
|
# Generate XML.
|
89
90
|
def xml
|
90
|
-
dir = @opts[:source]
|
91
|
-
PDD.log.info "Reading #{dir}"
|
91
|
+
dir = @opts[:source] || Dir.pwd
|
92
|
+
PDD.log.info "Reading from root dir #{dir}"
|
92
93
|
require_relative 'pdd/sources'
|
93
|
-
sources = Sources.new(dir)
|
94
|
-
@opts[:
|
95
|
-
|
96
|
-
end
|
97
|
-
@opts[:exclude]&.each do |p|
|
98
|
-
sources = sources.exclude(p)
|
99
|
-
PDD.log.info "Excluding #{p}"
|
100
|
-
end
|
94
|
+
sources = Sources.new(File.expand_path(dir))
|
95
|
+
sources.exclude((@opts[:exclude] || []) + (@opts['skip-gitignore'] || []))
|
96
|
+
sources.include(@opts[:include])
|
101
97
|
sanitize(
|
102
98
|
rules(
|
103
|
-
Nokogiri::XML::Builder.new do |xml|
|
99
|
+
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
|
104
100
|
xml << "<?xml-stylesheet type='text/xsl' href='#{xsl}'?>"
|
105
101
|
xml.puzzles(attrs) do
|
106
102
|
sources.fetch.each do |source|
|
@@ -152,16 +148,19 @@ module PDD
|
|
152
148
|
unless list.select { |r| r.start_with?('max-duplicates:') }.empty?
|
153
149
|
raise PDD::Error, 'You can\'t modify max-duplicates, it\'s always 1'
|
154
150
|
end
|
151
|
+
|
155
152
|
list.push('max-duplicates:1').map do |r|
|
156
153
|
name, value = r.split(':')
|
157
154
|
rule = RULES[name]
|
158
155
|
raise "Rule '#{name}' doesn't exist" if rule.nil?
|
156
|
+
|
159
157
|
rule.new(doc, value).errors.each do |e|
|
160
158
|
PDD.log.error e
|
161
159
|
total += 1
|
162
160
|
end
|
163
161
|
end
|
164
162
|
raise PDD::Error, "#{total} errors, see log above" unless total.zero?
|
163
|
+
|
165
164
|
xml
|
166
165
|
end
|
167
166
|
|
@@ -173,6 +172,7 @@ module PDD
|
|
173
172
|
errors.each { |e| PDD.log.error e }
|
174
173
|
PDD.log.error(xml) unless errors.empty?
|
175
174
|
raise SchemaError, errors.join('; ') unless errors.empty?
|
175
|
+
|
176
176
|
xml
|
177
177
|
end
|
178
178
|
end
|