File: error.rkt

package info (click to toggle)
racket-mode 20210916git0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 1,076 kB
  • sloc: lisp: 10,354; makefile: 58
file content (256 lines) | stat: -rw-r--r-- 10,107 bytes parent folder | download
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
#lang at-exp racket/base

(require (only-in pkg/db
                  get-catalogs)
         (only-in pkg/lib
                  pkg-catalog-suggestions-for-module
                  pkg-directory)
         racket/format
         racket/match
         racket/string
         setup/dirs
         "instrument.rkt"
         "stack-checkpoint.rkt"
         "util.rkt")

(provide display-exn
         racket-mode-error-display-handler
         prevent-path-elision-by-srcloc->string)

(module+ test
  (require rackunit))

(define (display-exn exn)
  (racket-mode-error-display-handler (exn-message exn) exn))

(define (racket-mode-error-display-handler str v)
  (cond [(exn? v)
         (unless (equal? "Check failure" (exn-message v)) ;rackunit check fails
           (display-commented (complete-paths
                               (undo-path->relative-string/library str)))
           (display-srclocs v)
           (unless (exn:fail:user? v)
             (display-context v))
           (maybe-suggest-packages v))]
        [else
         (display-commented str)]))

;;; srclocs

(define (display-srclocs exn)
  (when (exn:srclocs? exn)
    (define srclocs
      (match ((exn:srclocs-accessor exn) exn)
        ;; Some exceptions like exn:fail:read? include the first
        ;; srcloc in exn-message -- don't show it again.
        [(cons _ xs)
         #:when (or (exn:fail:read? exn)
                    (exn:fail:contract:variable? exn))
         xs]
        ;; Some exceptions like exn:fail:syntax? with Typed Racket
        ;; include _all_ in exn-message -- don't show _any_.
        [_
         #:when (exn:fail:syntax? exn)
         '()]
        [xs xs]))
    (for ([s (in-list srclocs)])
      (display-commented (source-location->string s)))))

(module+ test
  (let ([o (open-output-string)])
    (parameterize ([current-error-port o])
      (display-srclocs (make-exn:fail:read "..."
                                           (current-continuation-marks)
                                           '())))
    (check-equal? (get-output-string o) "")))

;; We don't use source-location->string from syntax/srcloc, because we
;; don't want the setup/path-to-relative behavior that elides complete
;; pathnames with prefixes like "<pkgs>/" etc. For strings we create
;; ourselves, we use our own such function, defined here.
(define (source-location->string x)
  (define src
    ;; Although I want to find/fix this properly upstream -- is
    ;; something a path-string? when it should be a path? -- for now
    ;; just catch here the case where the source is a string like
    ;; "\"/path/to/file.rkt\"" i.e. the string value has quotes.
    (match (srcloc-source x)
      [(pregexp "^\"(.+)\"$" (list _ unquoted)) unquoted]
      [v v]))
  (define line (or (srcloc-line x)   1))
  (define col  (or (srcloc-column x) 0))
  (format "~a:~a:~a" src line col))

;;; context

(define (display-context exn)
  (cond [(instrumenting-enabled)
         (define p (open-output-string))
         (print-error-trace p exn)
         (match (get-output-string p)
           ["" (void)]
           [s  (display-commented (~a "Context (errortrace):" s))])]
        [else
         (match (context->string
                 (continuation-mark-set->trimmed-context
                  (exn-continuation-marks exn)))
           ["" (void)]
           [s (display-commented
               (~a "Context (plain; to see better errortrace context, re-run with C-u prefix):\n"
                   s))])]))

(define (context->string xs)
  (string-join (for/list ([x xs]
                          [_ (error-print-context-length)])
                 (context-item->string x))
               "\n"))

(define (context-item->string ci)
  (match-define (cons id srcloc) ci)
  (~a (if (or srcloc id) "  " "")
      (if srcloc (source-location->string srcloc) "")
      (if (and srcloc id) " " "")
      (if id (format "~a" id) "")))

;;; Complete pathnames for Emacs

;; The background here is that want source locations in error messages
;; to use complete pathnames ("complete" as in complete-path? a.k.a.
;; "absolute" plus drive letter on Windows). That way, Emacs features
;; like compilation-mode's next-error command will work nicely.
;;
;; - When we create strings from srclocs, ourselves: We create them
;;   that way. See source-location->string defined/used in this file.
;;
;; - When other things create strings from scrlocs: We try to prevent
;;   them from eliding in the first place. And since we can't always
;;   prevent, we try to undo any elision baked into the error message
;;   by the time we get it. As a sanity check, we don't transform
;;   things into complete pathnames unless the result actually exists.

;; srcloc->string from racket/base uses current-directory-for-user to
;; elide paths. Setting that to 'pref-dir -- where it is very unlikely
;; a user's source file will be -- should prevent it from eliding
;; anything.
(define (prevent-path-elision-by-srcloc->string)
  (current-directory-for-user (find-system-path 'pref-dir)))

;; The source-location->string function provided by syntax/srcloc uses
;; path->relative-string/library to elide paths with prefixes like
;; <pkgs>/ or <collects>/. We avoid using that function in this
;; module, for example in display-srclocs and in context-item->string
;; above. However things like racket/contract use syntax/srcloc and
;; those prefixes might be baked into exn-message. Here we try to undo
;; this for things that look like such source locations.
(define (undo-path->relative-string/library s)
  (regexp-replace*
   #px"(<(.+?)>/(.+?)):(\\d+[:.]\\d+)"
   s
   (λ (_ prefix+rel-path prefix rel-path line+col)
     (define (f dir [rel rel-path])
       (existing (simplify-path (build-path dir rel))))
     (~a (or (and (path-string? rel-path)
                  (match prefix
                    ["collects" (f (find-collects-dir))]
                    ["user"     (f (find-user-collects-dir))]
                    ["doc"      (f (find-doc-dir))]
                    ["user-doc" (f (find-user-doc-dir))]
                    ["pkgs"     (match rel-path
                                  [(pregexp "^(.+?)/(.+?)$" (list _ pkg-name more))
                                   (f (pkg-directory pkg-name) more)]
                                  [_ #f])]
                    [_          #f]))
             prefix+rel-path) ;keep as-is
         ":" line+col))))

(module+ test
  (check-equal? (undo-path->relative-string/library "<collects>/racket/file.rkt:1:0:")
                (~a (build-path (find-collects-dir) "racket" "file.rkt") ":1:0:"))
  (check-equal? (undo-path->relative-string/library "<doc>/2d/index.html:1:0:")
                (~a (build-path (find-doc-dir) "2d" "index.html") ":1:0:"))
  ;; Note: No test for <user-doc> because unlikely to work on Travis CI
  (let ([non-existing "<collects>/racket/does-not-exist.rkt:1:0 blah blah blah"])
   (check-equal? (undo-path->relative-string/library non-existing)
                 non-existing
                 "does not change to non-existing pathname")))

(module+ test
  (let ()
    (local-require racket/path
                   setup/path-to-relative
                   drracket/find-module-path-completions)
    (define-values (_links _paths pkg-dirs)
      (alternate-racket-clcl/clcp (find-system-path 'exec-file) (box #f)))
    (printf "Checking .rkt files in ~v packages...\n" (length pkg-dirs))
    (define c (make-hash))
    (for ([item (in-list pkg-dirs)])
      (match item
        [(list (? string?) (? path? dir))
         (for ([p (in-directory dir)]
               #:when (equal? #".rkt" (path-get-extension p)))
           (define complete (~a p                                           ":1.0"))
           (define relative (~a (path->relative-string/library p #:cache c) ":1.0"))
           (define undone (undo-path->relative-string/library relative))
           (check-equal? undone complete))]
        [_ (void)]))))

;; If this looks like a source location where the pathname is
;; relative, prepend current-directory if that results in an actually
;; existing file.
(define (complete-paths s)
  (regexp-replace*
   #px"([^:]+):(\\d+[:.]\\d+)"
   s
   (λ (_ orig-path line+col)
     (~a (or (and (relative-path? orig-path)
                  (existing (build-path (current-directory) orig-path)))
             orig-path)
         ":" line+col))))

(define (existing p)
  (and (path? p) (file-exists? p) p))

(module+ test
  (let ()
    (local-require racket/file
                   racket/path)
    (define temp-dir (find-system-path 'temp-dir))
    (define example (make-temporary-file "racket-mode-test-~a" #f temp-dir))
    (define name (file-name-from-path example))
    (parameterize ([current-directory temp-dir])
      (let ([suffix ":3:0: f: unbound identifier\n   in: f"])
        (check-equal? (complete-paths (~a name suffix))
                      (~a (build-path temp-dir name) suffix)
                      "relative path: curdir prepended when that is an existing file"))
      (let ([msg (~a example ":3:0: f: unbound identifier\n   in: f")])
        (check-equal? (complete-paths msg)
                      msg
                      "already complete path: no change")))
    (delete-file example)))

;;; packages

(define (maybe-suggest-packages exn)
  (when (exn:missing-module? exn)
    (match (get-catalogs)
      [(list)
       (display-commented
        @~a{-----
            Can't suggest packages to install, because pkg/db get-catalogs is '().
            To configure:
            1. Start DrRacket.
            2. Choose "File | Package Manager".
            3. Click "Available from Catalog".
            4. When prompted, click "Update".
            -----})]
      [_
       (define mod ((exn:missing-module-accessor exn) exn))
       (match (pkg-catalog-suggestions-for-module mod)
         [(list) void]
         [(list p)
          (display-commented
           @~a{Try "raco pkg install @|p|" ?})]
         [(? list? ps)
          (display-commented
           @~a{Try "raco pkg install" one of @(string-join ps ", ") ?})]
         [_ void])])))