v / cmd / tools
Raw file | 691 loc (648 sloc) | 16.96 KB | Latest commit hash 2029d1830
1// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module main
5
6import os
7import os.cmdline
8import rand
9import term
10import v.help
11import regex
12
13const (
14 too_long_line_length_example = 120
15 too_long_line_length_codeblock = 120
16 too_long_line_length_table = 120
17 too_long_line_length_link = 150
18 too_long_line_length_other = 100
19 term_colors = term.can_show_color_on_stderr()
20 hide_warnings = '-hide-warnings' in os.args || '-w' in os.args
21 show_progress = os.getenv('GITHUB_JOB') == '' && '-silent' !in os.args
22 non_option_args = cmdline.only_non_options(os.args[2..])
23 is_verbose = os.getenv('VERBOSE') != ''
24 vcheckfolder = os.join_path(os.vtmp_dir(), 'v', 'vcheck_${os.getuid()}')
25 should_autofix = os.getenv('VAUTOFIX') != ''
26 vexe = @VEXE
27)
28
29struct CheckResult {
30pub mut:
31 warnings int
32 errors int
33 oks int
34}
35
36fn (v1 CheckResult) + (v2 CheckResult) CheckResult {
37 return CheckResult{
38 warnings: v1.warnings + v2.warnings
39 errors: v1.errors + v2.errors
40 oks: v1.oks + v2.oks
41 }
42}
43
44fn main() {
45 if non_option_args.len == 0 || '-help' in os.args {
46 help.print_and_exit('check-md')
47 exit(0)
48 }
49 if '-all' in os.args {
50 println('´-all´ flag is deprecated. Please use ´v check-md .´ instead.')
51 exit(1)
52 }
53 mut skip_line_length_check := '-skip-line-length-check' in os.args
54 if show_progress {
55 // this is intended to be replaced by the progress lines
56 println('')
57 }
58 mut files_paths := non_option_args.clone()
59 mut res := CheckResult{}
60 if term_colors {
61 os.setenv('VCOLORS', 'always', true)
62 }
63 os.mkdir_all(vcheckfolder, mode: 0o700) or {} // keep directory private
64 defer {
65 os.rmdir_all(vcheckfolder) or {}
66 }
67 for i := 0; i < files_paths.len; i++ {
68 file_path := files_paths[i]
69 if os.is_dir(file_path) {
70 files_paths << md_file_paths(file_path)
71 continue
72 }
73 real_path := os.real_path(file_path)
74 lines := os.read_lines(real_path) or {
75 println('"${file_path}" does not exist')
76 res.warnings++
77 continue
78 }
79 mut mdfile := MDFile{
80 skip_line_length_check: skip_line_length_check
81 path: file_path
82 lines: lines
83 }
84 res += mdfile.check()
85 }
86 if res.errors == 0 && show_progress {
87 clear_previous_line()
88 }
89 if res.warnings > 0 || res.errors > 0 || res.oks > 0 {
90 println('\nWarnings: ${res.warnings} | Errors: ${res.errors} | OKs: ${res.oks}')
91 }
92 if res.errors > 0 {
93 exit(1)
94 }
95}
96
97fn md_file_paths(dir string) []string {
98 mut files_to_check := []string{}
99 md_files := os.walk_ext(dir, '.md')
100 for file in md_files {
101 nfile := file.replace('\\', '/')
102 if nfile.contains_any_substr(['/thirdparty/', 'CHANGELOG']) {
103 continue
104 }
105 files_to_check << file
106 }
107 return files_to_check
108}
109
110fn wprintln(s string) {
111 if !hide_warnings {
112 println(s)
113 }
114}
115
116fn ftext(s string, cb fn (string) string) string {
117 if term_colors {
118 return cb(s)
119 }
120 return s
121}
122
123fn btext(s string) string {
124 return ftext(s, term.bold)
125}
126
127fn mtext(s string) string {
128 return ftext(s, term.magenta)
129}
130
131fn rtext(s string) string {
132 return ftext(s, term.red)
133}
134
135fn wline(file_path string, lnumber int, column int, message string) string {
136 return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(mtext(' warn:')) +
137 rtext(' ${message}')
138}
139
140fn eline(file_path string, lnumber int, column int, message string) string {
141 return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(rtext(' error: ${message}'))
142}
143
144const default_command = 'compile'
145
146struct VCodeExample {
147mut:
148 text []string
149 command string
150 sline int
151 eline int
152}
153
154enum MDFileParserState {
155 markdown
156 vexample
157 codeblock
158}
159
160struct MDFile {
161 path string
162 skip_line_length_check bool
163mut:
164 lines []string
165 examples []VCodeExample
166 current VCodeExample
167 state MDFileParserState = .markdown
168}
169
170fn (mut f MDFile) progress(message string) {
171 if show_progress {
172 clear_previous_line()
173 println('File: ${f.path:-30s}, Lines: ${f.lines.len:5}, ${message}')
174 }
175}
176
177fn (mut f MDFile) check() CheckResult {
178 mut res := CheckResult{}
179 mut anchor_data := AnchorData{}
180 for j, line in f.lines {
181 // f.progress('line: $j')
182 if !f.skip_line_length_check {
183 if f.state == .vexample {
184 if line.len > too_long_line_length_example {
185 wprintln(wline(f.path, j, line.len, 'example lines must be less than ${too_long_line_length_example} characters'))
186 wprintln(line)
187 res.warnings++
188 }
189 } else if f.state == .codeblock {
190 if line.len > too_long_line_length_codeblock {
191 wprintln(wline(f.path, j, line.len, 'code lines must be less than ${too_long_line_length_codeblock} characters'))
192 wprintln(line)
193 res.warnings++
194 }
195 } else if line.starts_with('|') {
196 if line.len > too_long_line_length_table {
197 wprintln(wline(f.path, j, line.len, 'table lines must be less than ${too_long_line_length_table} characters'))
198 wprintln(line)
199 res.warnings++
200 }
201 } else if line.contains('http') {
202 if line.all_after('https').len > too_long_line_length_link {
203 wprintln(wline(f.path, j, line.len, 'link lines must be less than ${too_long_line_length_link} characters'))
204 wprintln(line)
205 res.warnings++
206 }
207 } else if line.len > too_long_line_length_other {
208 eprintln(eline(f.path, j, line.len, 'must be less than ${too_long_line_length_other} characters'))
209 eprintln(line)
210 res.errors++
211 }
212 }
213 if f.state == .markdown {
214 anchor_data.add_links(j, line)
215 anchor_data.add_link_targets(j, line)
216 }
217
218 f.parse_line(j, line)
219 }
220 anchor_data.check_link_target_match(f.path, mut res)
221 res += f.check_examples()
222 return res
223}
224
225fn (mut f MDFile) parse_line(lnumber int, line string) {
226 if line.starts_with('```v') {
227 if f.state == .markdown {
228 f.state = .vexample
229 mut command := line.replace('```v', '').trim_space()
230 if command == '' {
231 command = default_command
232 } else if command == 'nofmt' {
233 command += ' ${default_command}'
234 }
235 f.current = VCodeExample{
236 sline: lnumber
237 command: command
238 }
239 }
240 return
241 }
242 if line.starts_with('```') {
243 match f.state {
244 .vexample {
245 f.state = .markdown
246 f.current.eline = lnumber
247 f.examples << f.current
248 f.current = VCodeExample{}
249 return
250 }
251 .codeblock {
252 f.state = .markdown
253 return
254 }
255 .markdown {
256 f.state = .codeblock
257 return
258 }
259 }
260 }
261 if f.state == .vexample {
262 f.current.text << line
263 }
264}
265
266struct Headline {
267 line int
268 lable string
269 level int
270}
271
272struct Anchor {
273 line int
274}
275
276type AnchorTarget = Anchor | Headline
277
278struct AnchorLink {
279 line int
280 lable string
281}
282
283struct AnchorData {
284mut:
285 links map[string][]AnchorLink
286 anchors map[string][]AnchorTarget
287}
288
289fn (mut ad AnchorData) add_links(line_number int, line string) {
290 query := r'\[(?P<lable>[^\]]+)\]\(\s*#(?P<link>[a-z0-9\-\_\x7f-\uffff]+)\)'
291 mut re := regex.regex_opt(query) or { panic(err) }
292 res := re.find_all_str(line)
293
294 for elem in res {
295 re.match_string(elem)
296 link := re.get_group_by_name(elem, 'link')
297 ad.links[link] << AnchorLink{
298 line: line_number
299 lable: re.get_group_by_name(elem, 'lable')
300 }
301 }
302}
303
304fn (mut ad AnchorData) add_link_targets(line_number int, line string) {
305 if line.trim_space().starts_with('#') {
306 if headline_start_pos := line.index(' ') {
307 headline := line.substr(headline_start_pos + 1, line.len)
308 link := create_ref_link(headline)
309 ad.anchors[link] << Headline{
310 line: line_number
311 lable: headline
312 level: headline_start_pos
313 }
314 }
315 } else {
316 query := '<a\\s*id=["\'](?P<link>[a-z0-9\\-\\_\\x7f-\\uffff]+)["\']\\s*/>'
317 mut re := regex.regex_opt(query) or { panic(err) }
318 res := re.find_all_str(line)
319
320 for elem in res {
321 re.match_string(elem)
322 link := re.get_group_by_name(elem, 'link')
323 ad.anchors[link] << Anchor{
324 line: line_number
325 }
326 }
327 }
328}
329
330fn (mut ad AnchorData) check_link_target_match(fpath string, mut res CheckResult) {
331 mut checked_headlines := []string{}
332 mut found_error_warning := false
333 for link, linkdata in ad.links {
334 if link in ad.anchors {
335 checked_headlines << link
336 if ad.anchors[link].len > 1 {
337 found_error_warning = true
338 res.errors++
339 for anchordata in ad.anchors[link] {
340 eprintln(eline(fpath, anchordata.line, 0, 'multiple link targets of existing link (#${link})'))
341 }
342 }
343 } else {
344 found_error_warning = true
345 res.errors++
346 for brokenlink in linkdata {
347 eprintln(eline(fpath, brokenlink.line, 0, 'no link target found for existing link [${brokenlink.lable}](#${link})'))
348 }
349 }
350 }
351 for link, anchor_lists in ad.anchors {
352 if link !in checked_headlines {
353 if anchor_lists.len > 1 {
354 for anchor in anchor_lists {
355 line := match anchor {
356 Headline {
357 anchor.line
358 }
359 Anchor {
360 anchor.line
361 }
362 }
363 wprintln(wline(fpath, line, 0, 'multiple link target for non existing link (#${link})'))
364 found_error_warning = true
365 res.warnings++
366 }
367 }
368 }
369 }
370 if found_error_warning {
371 eprintln('') // fix suppressed last error output
372 }
373}
374
375// based on a reference sample md doc
376// https://github.com/aheissenberger/vlang-markdown-module/blob/master/test.md
377fn create_ref_link(s string) string {
378 mut result := ''
379 for c in s.trim_space() {
380 result += match c {
381 `a`...`z`, `0`...`9` {
382 c.ascii_str()
383 }
384 `A`...`Z` {
385 c.ascii_str().to_lower()
386 }
387 ` `, `-` {
388 '-'
389 }
390 `_` {
391 '_'
392 }
393 else {
394 if c > 127 { c.ascii_str() } else { '' }
395 }
396 }
397 }
398 return result
399}
400
401fn (mut f MDFile) debug() {
402 for e in f.examples {
403 eprintln('f.path: ${f.path} | example: ${e}')
404 }
405}
406
407fn cmdexecute(cmd string) int {
408 verbose_println(cmd)
409 res := os.execute(cmd)
410 if res.exit_code < 0 {
411 return 1
412 }
413 if res.exit_code != 0 {
414 eprint(res.output)
415 }
416 return res.exit_code
417}
418
419fn silent_cmdexecute(cmd string) int {
420 verbose_println(cmd)
421 res := os.execute(cmd)
422 return res.exit_code
423}
424
425fn get_fmt_exit_code(vfile string, vexe string) int {
426 return silent_cmdexecute('${os.quoted_path(vexe)} fmt -verify ${os.quoted_path(vfile)}')
427}
428
429fn (mut f MDFile) check_examples() CheckResult {
430 mut errors := 0
431 mut oks := 0
432 recheck_all_examples: for e in f.examples {
433 if e.command == 'ignore' {
434 continue
435 }
436 if e.command == 'wip' {
437 continue
438 }
439 fname := os.base(f.path).replace('.md', '_md')
440 uid := rand.ulid()
441 cfile := os.join_path(vcheckfolder, '${uid}.c')
442 vfile := os.join_path(vcheckfolder, 'check_${fname}_example_${e.sline}__${e.eline}__${uid}.v')
443 efile := os.join_path(vcheckfolder, 'check_${fname}_example_${e.sline}__${e.eline}__${uid}.exe')
444 mut should_cleanup_vfile := true
445 // eprintln('>>> checking example $vfile ...')
446 vcontent := e.text.join('\n') + '\n'
447 os.write_file(vfile, vcontent) or { panic(err) }
448 mut acommands := e.command.split(' ')
449 nofmt := 'nofmt' in acommands
450 for command in acommands {
451 f.progress('example from ${e.sline} to ${e.eline}, command: ${command}')
452 fmt_res := if nofmt { 0 } else { get_fmt_exit_code(vfile, vexe) }
453 match command {
454 'compile' {
455 res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(efile)} ${os.quoted_path(vfile)}')
456 if res != 0 || fmt_res != 0 {
457 if res != 0 {
458 eprintln(eline(f.path, e.sline, 0, 'example failed to compile'))
459 }
460 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
461 unsafe {
462 goto recheck_all_examples
463 }
464 }
465 eprintln(vcontent)
466 should_cleanup_vfile = false
467 errors++
468 continue
469 }
470 oks++
471 }
472 'cgen' {
473 res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
474 if res != 0 || fmt_res != 0 {
475 if res != 0 {
476 eprintln(eline(f.path, e.sline, 0, 'example failed to generate C code'))
477 }
478 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
479 unsafe {
480 goto recheck_all_examples
481 }
482 }
483 eprintln(vcontent)
484 should_cleanup_vfile = false
485 errors++
486 continue
487 }
488 oks++
489 }
490 'globals' {
491 res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -enable-globals -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
492 if res != 0 || fmt_res != 0 {
493 if res != 0 {
494 eprintln(eline(f.path, e.sline, 0, '`example failed to compile with -enable-globals'))
495 }
496 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
497 unsafe {
498 goto recheck_all_examples
499 }
500 }
501 eprintln(vcontent)
502 should_cleanup_vfile = false
503 errors++
504 continue
505 }
506 oks++
507 }
508 'live' {
509 res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -live -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
510 if res != 0 || fmt_res != 0 {
511 if res != 0 {
512 eprintln(eline(f.path, e.sline, 0, 'example failed to compile with -live'))
513 }
514 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
515 unsafe {
516 goto recheck_all_examples
517 }
518 }
519 eprintln(vcontent)
520 should_cleanup_vfile = false
521 errors++
522 continue
523 }
524 oks++
525 }
526 'shared' {
527 res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -shared -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
528 if res != 0 || fmt_res != 0 {
529 if res != 0 {
530 eprintln(eline(f.path, e.sline, 0, 'module example failed to compile with -shared'))
531 }
532 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
533 unsafe {
534 goto recheck_all_examples
535 }
536 }
537 eprintln(vcontent)
538 should_cleanup_vfile = false
539 errors++
540 continue
541 }
542 oks++
543 }
544 'failcompile' {
545 res := silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
546 if res == 0 || fmt_res != 0 {
547 if res == 0 {
548 eprintln(eline(f.path, e.sline, 0, '`failcompile` example compiled'))
549 }
550 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
551 unsafe {
552 goto recheck_all_examples
553 }
554 }
555 eprintln(vcontent)
556 should_cleanup_vfile = false
557 errors++
558 continue
559 }
560 oks++
561 }
562 'oksyntax' {
563 res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}')
564 if res != 0 || fmt_res != 0 {
565 if res != 0 {
566 eprintln(eline(f.path, e.sline, 0, '`oksyntax` example with invalid syntax'))
567 }
568 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
569 unsafe {
570 goto recheck_all_examples
571 }
572 }
573 eprintln(vcontent)
574 should_cleanup_vfile = false
575 errors++
576 continue
577 }
578 oks++
579 }
580 'okfmt' {
581 if fmt_res != 0 {
582 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
583 unsafe {
584 goto recheck_all_examples
585 }
586 }
587 eprintln(vcontent)
588 should_cleanup_vfile = false
589 errors++
590 continue
591 }
592 oks++
593 }
594 'badsyntax' {
595 res := silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}')
596 if res == 0 {
597 eprintln(eline(f.path, e.sline, 0, '`badsyntax` example can be parsed fine'))
598 eprintln(vcontent)
599 should_cleanup_vfile = false
600 errors++
601 continue
602 }
603 oks++
604 }
605 'nofmt' {}
606 // mark the example as playable inside docs
607 'play' {}
608 // same as play, but run example as a test
609 'play-test' {}
610 // when ```vmod
611 'mod' {}
612 else {
613 eprintln(eline(f.path, e.sline, 0, 'unrecognized command: "${command}", use one of: wip/ignore/compile/failcompile/okfmt/nofmt/oksyntax/badsyntax/cgen/globals/live/shared'))
614 should_cleanup_vfile = false
615 errors++
616 }
617 }
618 }
619 os.rm(cfile) or {}
620 os.rm(efile) or {}
621 if should_cleanup_vfile {
622 os.rm(vfile) or { panic(err) }
623 }
624 }
625 return CheckResult{
626 errors: errors
627 oks: oks
628 }
629}
630
631fn verbose_println(message string) {
632 if is_verbose {
633 println(message)
634 }
635}
636
637fn clear_previous_line() {
638 if is_verbose {
639 return
640 }
641 term.clear_previous_line()
642}
643
644fn (mut f MDFile) report_not_formatted_example_if_needed(e VCodeExample, fmt_res int, vfile string) ! {
645 if fmt_res == 0 {
646 return
647 }
648 eprintln(eline(f.path, e.sline, 0, 'example is not formatted'))
649 if !should_autofix {
650 return
651 }
652 f.autofix_example(e, vfile) or {
653 if err is ExampleWasRewritten {
654 eprintln('>> f.path: ${f.path} | example from ${e.sline} to ${e.eline} was re-formated by vfmt')
655 return err
656 }
657 eprintln('>> f.path: ${f.path} | encountered error while autofixing the example: ${err}')
658 }
659}
660
661struct ExampleWasRewritten {
662 Error
663}
664
665fn (mut f MDFile) autofix_example(e VCodeExample, vfile string) ! {
666 eprintln('>>> AUTOFIXING f.path: ${f.path} | e.sline: ${e.sline} | vfile: ${vfile}')
667 res := cmdexecute('${os.quoted_path(vexe)} fmt -w ${os.quoted_path(vfile)}')
668 if res != 0 {
669 return error('could not autoformat the example')
670 }
671 formatted_content_lines := os.read_lines(vfile) or { return }
672 mut new_lines := []string{}
673 new_lines << f.lines#[0..e.sline + 1]
674 new_lines << formatted_content_lines
675 new_lines << f.lines#[e.eline..]
676 f.update_examples(new_lines)!
677 os.rm(vfile) or {}
678 f.examples = f.examples.filter(it.sline >= e.sline)
679 return ExampleWasRewritten{}
680}
681
682fn (mut f MDFile) update_examples(new_lines []string) ! {
683 os.write_file(f.path, new_lines.join('\n'))!
684 f.lines = new_lines
685 f.examples = []
686 f.current = VCodeExample{}
687 f.state = .markdown
688 for j, line in f.lines {
689 f.parse_line(j, line)
690 }
691}