/
critic.lisp
353 lines (281 loc) · 12.6 KB
/
critic.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
(uiop:define-package #:40ants-critic
(:use #:cl)
(:nicknames #:40ants-critic/critic)
(:import-from #:lisp-critic)
(:import-from #:40ants-doc
#:defsection-copy
#:defsection)
(:import-from #:docs-config
#:docs-config)
(:import-from #:40ants-critic/forms-reader
#:read-forms)
(:export #:critique-asdf-system
#:@index
#:@readme))
(in-package #:40ants-critic)
(defun ensure-asdf-system (name-or-system)
(etypecase name-or-system
(asdf:system name-or-system)
(cons
(case (first name-or-system)
(:version
(ensure-asdf-system
(second name-or-system)))
(:feature
;; Just ignore these kinds of dependencies for now
NIL)
(t
(error "Unknown form of dependency: ~S"
name-or-system))))
((or symbol string)
(asdf:find-system name-or-system))))
(defun asdf-system-files (system)
(let ((primary-system-name (asdf:primary-system-name system))
(results nil))
(labels ((recurse (name)
(let ((system (ensure-asdf-system name)))
(when (and system
;; We only interested in components of the
;; same ASDF primary system, because
;; we don't want to critic all system dependencies:
(string-equal (asdf:primary-system-name system)
primary-system-name))
(append (asdf:module-components system)
(loop for component in (asdf:component-sideway-dependencies system)
;; for component-system = (asdf:find-system component)
append (recurse component)))))))
(loop for component in (recurse system)
when (typep component 'asdf:cl-source-file)
do (pushnew (asdf:component-pathname component)
results
:test #'equal))
(values results))))
(defun critique-name (note)
"Returns the \"critique\" code as a lowercased string"
(string-downcase (symbol-name (lisp-critic::critique-name note))))
(defun remove-ignored (critics ignore)
(flet ((should-be-ignored (critique)
(let ((code (critique-name critique)))
(member code ignore
:test #'string-equal))))
(remove-if #'should-be-ignored critics)))
(defun make-response-string (name response blist)
(let ((format-string (lisp-critic:response-format-string response))
(pattern (extend-match::instantiate-pattern (lisp-critic::response-args response)
blist)))
(format nil "~&[~A]: ~?"
(string-downcase (symbol-name name))
format-string
pattern)))
(defun print-critique-response (critique
&optional (stream *standard-output*))
(let ((name (lisp-critic::critique-name critique))
(blist (lisp-critic::critique-blist critique))
(code (lisp-critic::critique-code critique)))
(let ((response (lisp-critic::get-response name)))
(cond ((null response)
(let ((*print-lines* 2) (*print-pretty* t)
(*print-right-margin* lisp-critic::*output-width*))
(format stream "~&~A: Code: ~W" name code)))
(t
(write-wrap:write-wrap stream
(make-response-string name response blist)
lisp-critic::*output-width*)))
(lisp-critic::print-separator stream))))
(defun print-critique-responses (critiques
&optional (stream *standard-output*))
(let ((*print-pretty* nil))
(when critiques
(lisp-critic::print-separator stream))
(dolist (critique critiques)
(print-critique-response critique stream))))
;; This critique is wrong, because it relates to the DO form,
;; not to the LOOP's DO:
;; ignore-critiques: do-with-body
;;
;; Function is a little bit long, but not critical yet:
;; ignore-critiques: function-too-long
(defun critique-file (filename &key (out *standard-output*)
(names (lisp-critic:get-pattern-names))
(ignore nil))
"Returns a number of found problems."
(let ((filename-already-printed nil)
(problems-count 0))
(loop for (code form-ignore package) in (read-forms filename)
for all-critiques = (lisp-critic::generate-critiques code names)
for critiques = (remove-ignored all-critiques
(append ignore
form-ignore))
when critiques
do (unless filename-already-printed
(pprint filename out)
(setf filename-already-printed t))
(lisp-critic::print-separator out #\*)
(let ((*print-right-margin* lisp-critic::*output-width*)
(*package* package))
(pprint code out))
(print-critique-responses critiques out)
(incf problems-count
(length critiques)))
(values problems-count)))
(defun get-blacklist (whitelist)
"Returns list of all LISP-CRITIC patterns, excluding WHITELIST arguments."
(loop for pattern in (lisp-critic:get-pattern-names)
unless (member pattern whitelist :test #'string-equal)
collect (string-downcase pattern)))
(defun critique-asdf-system (name &key
(out *standard-output*)
(ignore nil)
(whitelist nil))
"Outputs advices on how given ASDF system can be improved.
This function analyzes all lisp files of the given system and
outputs advices on how code might be improved.
NAME argument should be a string or symbol designator of ASDF system.
IGNORE argument can be a list of string. Each string should be a code
shown in the square brackets in the critique output.
WHITELIST argument can be a list of string. Each string should be a code
shown in the square brackets in the critique output.
Only IGNORE or WHITELIST can be used. Not both at the same time.
OUT argument is optional. It should be an output stream to write
advices to.
Result of the function is number of found problems."
#+quicklisp
(ql:quickload name :silent t)
#-quicklisp
(asdf:load-system name)
(when (and ignore whitelist)
(error "Please only specify IGNORE or WHITELIST, not both"))
(loop for filename in (asdf-system-files name)
for num-problems = (critique-file
filename
:out out
:ignore (cond (whitelist (get-blacklist whitelist))
(ignore ignore)
(t nil)))
summing num-problems))
;;;;;;;;;;;;;;;;;;;
;; Documentation ;;
;;;;;;;;;;;;;;;;;;;
(defmethod docs-config ((system (eql (asdf:find-system "40ants-critic"))))
;; 40ANTS-DOC-THEME-40ANTS system will bring
;; as dependency a full 40ANTS-DOC but we don't want
;; unnecessary dependencies here:
#+quicklisp
(uiop:symbol-call :ql :quickload :40ants-doc-theme-40ants)
#-quicklisp
(asdf:load-system :40ants-doc-theme-40ants)
(list :theme
(find-symbol "40ANTS-THEME"
(find-package "40ANTS-DOC-THEME-40ANTS"))))
(defsection @index (:title "40ANTS-CRITIC"
:ignore-words ("CI"
"ASDF"
"MIT"
"LISP-CRITIC"
"LISP-CRITIC:CRITIQUE-FILE")
:external-docs ("https://40ants.com/ci/")
:external-links (("LISP-CRITIC" . "https://github.com/g000001/lisp-critic")))
(40ants-critic system)
(@installation section)
(@usage section)
(@api section))
(defsection-copy @readme @index)
(defsection @installation (:title "Installation"
:external-docs ("https://40ants.com/ci/"))
"This system can be installed from [Ultralisp](https://ultralisp.org) like this:
```lisp
(ql-dist:install-dist \"http://dist.ultralisp.org/\"
:prompt nil)
```
If you are going to use this utility from a command line, then you might install it
using [Roswell](https://github.com/roswell/roswell):
```bash
$ ros install 40ants/40ants-critic
Installing from github 40ants/40ants-critic
To load \"40ants-critic\":
Load 1 ASDF system:
40ants-critic
; Loading \"40ants-critic\"
; compiling file \"/Users/art/.roswell/local-projects/40ants/critic/src/critic.lisp\" (written 20 FEB 2022 12:54:52 PM):
; wrote /Users/art/.cache/common-lisp/sbcl-2.1.11-macosx-x64/Users/art/.roswell/local-projects/40ants/critic/src/critic-tmp5GEXGEG5.fasl
; compilation finished in 0:00:00.026
[1/3] System '40ants-critic' found. Loading the system..
[2/3] Processing build-hook..
[3/3] Attempting to install the scripts in roswell/ subdirectory of the system...
Found 1 scripts: lisp-critic
/Users/art/.roswell/bin/lisp-critic
```
Also, you might use this checker in your CI pipeline on the GitHub.
It might check all pull-requests to ensure the code will remain clean.
To learn more about using it as a part of the GitHub workflow, read
40ANTS-CI-DOCS/INDEX::@CRITIC section.
")
(defsection @usage (:title "Usage")
"This wrapper provides a simple way to analyze code of a single ASDF system.
To get some advices, use CRITIQUE-ASDF-SYSTEM function. Difference between
this function and LISP-CRITIC:CRITIQUE-FILE function is that the latter
outputs all forms from the file even if there is no any advices.
CRITIQUE-ASDF-SYSTEM has IGNORE and WHITELIST keyword parameters. The
arguments can be a list of strings. Each string should be a code
shown in the square brackets in the critique output. IGNORE arguments will
be ignored, while WHITELIST arguments will be the only results. You can
only supply either IGNORE or WHITELIST, not both.
```lisp
(critique-asdf-system :lisp-critic :ignore '(\"let*-single\"))
(critique-asdf-system :lisp-critic :whitelist '(\"let*-single\" \"needless-shiftf\"))
```
Also, CRITIQUE-ASDF-SYSTEM returns a number of found problems which is useful
for CI pipelines. For example, `lisp-critic` script uses this number to report
that the unix command was failed:
```bash
lisp-critic reblocks-text-editor
#P\"/Users/art/projects/lisp/zibaldone/src/utils/text.lisp\"
**********************************************************************
(DEFUN REMOVE-HTML-TAGS (HTML-STRING)
(LET* ((RESULT
(CL-PPCRE:REGEX-REPLACE-ALL \"<[^>]+>\" HTML-STRING \"\")))
(IF (STRING= RESULT +ZERO-WIDTH-SPACE+)
RESULT
(CL-PPCRE:REGEX-REPLACE-ALL +ZERO-WIDTH-SPACE+ RESULT \"\"))))
----------------------------------------------------------------------
[let*-single]: There's no need for LET* here. Use LET unless you can't.
----------------------------------------------------------------------
```
You can ignore all `let*-single` warnings by adding `--ignore 'let*-single'`
command line option or put a special comment before the top-level form:
You can ignore all `let*-single` warnings by adding `--ignore 'let*-single'`
```bash
lisp-critic --ignore 'let*-single' lisp-critic
```
or ignore all `if-no-else` and `needless-shiftf` warnings by adding
```bash
lisp-critic --ignore 'if-no-else,needless-shiftf' lisp-critic
```
in the command line. Alternatively you can use the short version `-i`
instead of `--ignore`.
You can whitelist `let*-single` warnings by adding `--whitelist 'let*-single'`
```bash
lisp-critic --whitelist 'let*-single' lisp-critic
```
or whitelist `if-no-else` and `needless-shiftf` warnings by adding
```bash
lisp-critic --whitelist 'if-no-else,needless-shiftf' lisp-critic
```
in the command line. Alternatively you can use the short version `-w`
instead of `--whitelist`.
```bash
lisp-critic -w 'let*-single' lisp-critic
lisp-critic -w 'if-no-else,needless-shiftf' lisp-critic
```
To ignore a top-level-form, you can put a special comment before:
```lisp
;; ignore-critiques: let*-single
(defun remove-html-tags (html-string)
(let* ((result
...
```
Such comment can enumerate a multiple comma-separated critiques names.
"
)
(defsection @api (:title "API")
(critique-asdf-system function))