sloplint 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +329 -1
- data/README.md +38 -10
- data/docs/SPEC.md +48 -10
- data/exe/sloplint +13 -0
- data/lib/sloplint/cli.rb +45 -12
- data/lib/sloplint/engine.rb +7 -8
- data/lib/sloplint/rules.rb +2273 -22
- data/lib/sloplint/version.rb +1 -1
- metadata +4 -4
- data/bin/sloplint +0 -5
data/exe/sloplint
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Integer compare, not string: "3.10" < "3.3" is true and would reject a future
|
|
5
|
+
# Ruby. Array has <=> but no <, hence the explicit compare. Not Gem::Version --
|
|
6
|
+
# this has to hold when RubyGems is not loaded.
|
|
7
|
+
if (RUBY_VERSION.split(".").map(&:to_i).first(2) <=> [3, 3]) < 0
|
|
8
|
+
abort "sloplint needs Ruby 3.3 or later, found #{RUBY_VERSION}.\n" \
|
|
9
|
+
"macOS ships Ruby 2.6 at /usr/bin/ruby. Install a current one with `brew install ruby`."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require_relative "../lib/sloplint/cli"
|
|
13
|
+
exit Sloplint::CLI.run(ARGV)
|
data/lib/sloplint/cli.rb
CHANGED
|
@@ -10,7 +10,8 @@ require_relative "output"
|
|
|
10
10
|
module Sloplint
|
|
11
11
|
# Command-line shell: optparse, subcommands, exit codes. See docs/SPEC.md.
|
|
12
12
|
#
|
|
13
|
-
# Exit codes: 0 ran/no notes, 1 ran/notes found, 2 bad arguments.
|
|
13
|
+
# Exit codes: 0 ran/no notes, 1 ran/notes found, 2 bad arguments. Empty
|
|
14
|
+
# input is a 2 as well: an unread draft must not report as a clean one.
|
|
14
15
|
module CLI
|
|
15
16
|
module_function
|
|
16
17
|
|
|
@@ -18,7 +19,14 @@ module Sloplint
|
|
|
18
19
|
opts = { format: "full" }
|
|
19
20
|
parser = global_parser(opts, out:)
|
|
20
21
|
# Split global options from the subcommand and its args.
|
|
21
|
-
|
|
22
|
+
begin
|
|
23
|
+
parser.order!(argv)
|
|
24
|
+
rescue OptionParser::InvalidOption => e
|
|
25
|
+
# `check` is the default command, so its options are accepted before
|
|
26
|
+
# any command word: `sloplint --markdown -`. The global parser does not
|
|
27
|
+
# know them, so put the option back and let check's parser judge it.
|
|
28
|
+
argv.unshift("check", *e.args)
|
|
29
|
+
end
|
|
22
30
|
return 0 if opts[:help_shown] || opts[:version_shown]
|
|
23
31
|
|
|
24
32
|
command = argv.shift
|
|
@@ -45,15 +53,17 @@ module Sloplint
|
|
|
45
53
|
# ── check ───────────────────────────────────────────────────────────────
|
|
46
54
|
def cmd_check(argv, opts, out:, err:, stdin:)
|
|
47
55
|
markdown = false
|
|
56
|
+
strict = false
|
|
48
57
|
select = nil
|
|
49
58
|
ignore = nil
|
|
50
59
|
p = OptionParser.new do |o|
|
|
51
60
|
o.banner = "usage: sloplint check [options] [paths...] (\"-\" or no paths = stdin)"
|
|
52
61
|
o.on("-o", "--output-format FORMAT", %w[full json],
|
|
53
62
|
"Output format: 'full' or 'json' (may also be given before the command).") { |v| opts[:format] = v }
|
|
54
|
-
o.on("--markdown", "Skip fenced/inline code spans and URLs before scanning.") { markdown = true }
|
|
63
|
+
o.on("--markdown", "Skip fenced/inline code spans, HTML comments, and URLs before scanning.") { markdown = true }
|
|
55
64
|
o.on("--select IDS", "Only run these rules (comma-separated rule ids or category names).") { |v| select = v.split(",").map(&:strip) }
|
|
56
65
|
o.on("--ignore IDS", "Skip these rules (comma-separated rule ids or category names).") { |v| ignore = v.split(",").map(&:strip) }
|
|
66
|
+
o.on("--strict", "Run every rule, including the ones that are off by default.") { strict = true }
|
|
57
67
|
end
|
|
58
68
|
p.order!(argv)
|
|
59
69
|
|
|
@@ -64,24 +74,42 @@ module Sloplint
|
|
|
64
74
|
return 2
|
|
65
75
|
end
|
|
66
76
|
|
|
67
|
-
rules = select_rules(select, ignore)
|
|
77
|
+
rules = select_rules(select, ignore, strict)
|
|
68
78
|
paths = argv.empty? ? ["-"] : argv
|
|
69
79
|
by_path = paths.reject { |x| x == "-" }.size > 1
|
|
70
80
|
|
|
71
|
-
|
|
81
|
+
sources = []
|
|
72
82
|
paths.each do |path|
|
|
83
|
+
# Read as UTF-8 whatever the locale says. A sandbox with no LANG set
|
|
84
|
+
# leaves Ruby's default external encoding at US-ASCII, and then the
|
|
85
|
+
# first em dash raises "invalid byte sequence in US-ASCII" -- on prose
|
|
86
|
+
# that is perfectly valid UTF-8. Prose is the only input sloplint
|
|
87
|
+
# takes, so UTF-8 is the assumption, not the locale's guess.
|
|
73
88
|
text =
|
|
74
89
|
if path == "-"
|
|
75
|
-
stdin.read
|
|
90
|
+
stdin.read.force_encoding(Encoding::UTF_8)
|
|
76
91
|
else
|
|
77
92
|
unless File.file?(path)
|
|
78
93
|
err.puts("sloplint: no such file: #{path}")
|
|
79
94
|
return 2
|
|
80
95
|
end
|
|
81
|
-
File.read(path)
|
|
96
|
+
File.read(path, encoding: Encoding::UTF_8)
|
|
82
97
|
end
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
sources << [path == "-" ? "-" : path, text]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Tested on the raw text, before --markdown blanks code and URLs: a file
|
|
102
|
+
# that holds only a fenced code block did arrive, and scanning it clean
|
|
103
|
+
# is right. Nothing arriving at all is the trap -- the same one a
|
|
104
|
+
# mistyped rule id sets, and it exits 2 for the same reason.
|
|
105
|
+
if sources.all? { |_, text| text.strip.empty? }
|
|
106
|
+
names = sources.map { |label, _| label == "-" ? "stdin" : label }
|
|
107
|
+
err.puts("sloplint: empty input: nothing to check in #{names.join(", ")}")
|
|
108
|
+
return 2
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
all_notes = sources.flat_map do |label, text|
|
|
112
|
+
Engine.scan(text, rules:, markdown:, path: label)
|
|
85
113
|
end
|
|
86
114
|
|
|
87
115
|
case opts[:format]
|
|
@@ -93,7 +121,10 @@ module Sloplint
|
|
|
93
121
|
end
|
|
94
122
|
|
|
95
123
|
all_notes.empty? ? 0 : 1
|
|
96
|
-
|
|
124
|
+
# Invalid UTF-8 reaches this two ways: String#strip in the empty check
|
|
125
|
+
# raises Encoding::CompatibilityError, the engine's regexes raise
|
|
126
|
+
# ArgumentError. Both are the same thing to the reader.
|
|
127
|
+
rescue ArgumentError, Encoding::CompatibilityError => e
|
|
97
128
|
err.puts("sloplint: invalid input: #{e.message}")
|
|
98
129
|
2
|
|
99
130
|
end
|
|
@@ -168,9 +199,11 @@ module Sloplint
|
|
|
168
199
|
|
|
169
200
|
# --select/--ignore accept rule ids or category names. Default set excludes
|
|
170
201
|
# default_on:false rules unless they are explicitly selected.
|
|
171
|
-
def select_rules(select, ignore)
|
|
202
|
+
def select_rules(select, ignore, strict = false)
|
|
172
203
|
rules = if select
|
|
173
204
|
RULES.select { |r| select.include?(r.id) || select.include?(r.category) }
|
|
205
|
+
elsif strict
|
|
206
|
+
RULES
|
|
174
207
|
else
|
|
175
208
|
RULES.select(&:default_on)
|
|
176
209
|
end
|
|
@@ -187,7 +220,7 @@ module Sloplint
|
|
|
187
220
|
|
|
188
221
|
# Recommended for agents:
|
|
189
222
|
cat FILE | sloplint check --markdown -o json -
|
|
190
|
-
# exit 0 = clean, 1 = notes found, >1 = error
|
|
223
|
+
# exit 0 = clean, 1 = notes found, >1 = error (empty input is an error)
|
|
191
224
|
# each note: {path,line,column,severity,rule,category,message,excerpt,context,rationale,suggestion}
|
|
192
225
|
|
|
193
226
|
usage: sloplint [-o full|json] [command] [args]
|
data/lib/sloplint/engine.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Sloplint
|
|
|
14
14
|
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
|
-
# text: the source. rules: which Rule objects to run. markdown: blank code
|
|
17
|
+
# text: the source. rules: which Rule objects to run. markdown: blank code, HTML comments and URLs first.
|
|
18
18
|
# path: label carried into each Note (e.g. filename or "-" for stdin).
|
|
19
19
|
def scan(text, rules: RULES, markdown: false, path: "-")
|
|
20
20
|
source = text
|
|
@@ -95,14 +95,13 @@ module Sloplint
|
|
|
95
95
|
starts
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Replace fenced code, inline code, and URLs with same-length
|
|
99
|
-
# line/column stay correct. Newlines are preserved.
|
|
98
|
+
# Replace fenced code, HTML comments, inline code, and URLs with same-length
|
|
99
|
+
# whitespace so line/column stay correct. Newlines are preserved. One pass
|
|
100
|
+
# with one alternation, so whichever construct opens first is the one that
|
|
101
|
+
# gets consumed: a `<!--` quoted inside backticks is inline code, and a
|
|
102
|
+
# backtick inside a comment is part of the comment.
|
|
100
103
|
def blank_markdown(text)
|
|
101
|
-
|
|
102
|
-
text
|
|
103
|
-
.gsub(/```.*?```/m) { |s| blank.call(s) } # fenced code
|
|
104
|
-
.gsub(/`[^`\n]*`/) { |s| blank.call(s) } # inline code
|
|
105
|
-
.gsub(%r{https?://\S+}) { |s| blank.call(s) } # bare URLs
|
|
104
|
+
text.gsub(/```.*?```|<!--.*?-->|`[^`\n]*`|https?:\/\/\S+/m) { |s| s.gsub(/[^\n]/, " ") }
|
|
106
105
|
end
|
|
107
106
|
end
|
|
108
107
|
end
|