#lang at-exp racket/base (require racket/format racket/match (only-in racket/path path-only) racket/runtime-path racket/string setup/collects setup/dirs "fresh-line.rkt" "instrument.rkt" "util.rkt") (provide display-exn our-error-display-handler show-full-path-in-errors) (module+ test (require rackunit)) (define (display-exn exn) (our-error-display-handler (exn-message exn) exn)) (define (our-error-display-handler str v) (cond [(exn? v) (unless (equal? "Check failure" (exn-message v)) ;rackunit check fails (fresh-line) (display-commented (fully-qualify-error-path str)) (display-srclocs v) (unless (exn:fail:user? v) (display-context v)) (maybe-suggest-packages v))] [else (fresh-line) (display-commented str)])) (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))))) (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 (string-append "Context (errortrace):" ;; et prepends a \n s))])] [else (match (context->string (continuation-mark-set->context (exn-continuation-marks exn))) ["" (void)] [s (display-commented (string-append "Context:\n" s))])])) (define (context->string xs) ;; Limit the context in two ways: ;; 1. Don't go beyond error-print-context-length ;; 2. Don't go into "system" context that's just noisy. (string-join (for/list ([x xs] [_ (error-print-context-length)] #:unless (system-context? x)) (context-item->string x)) "\n")) (define-runtime-path here "error.rkt") (define (system-context? ci) (match-define (cons id src) ci) (or (not src) (let ([src (srcloc-source src)]) (and (path? src) (or (equal? (path-only src) (path-only here)) (under-system-path? src)))))) (define (under-system-path? path) (match (path->collects-relative path) [`(collects #"mred" . ,_) #t] [`(collects #"racket" #"contract" . ,_) #t] [`(collects #"racket" #"private" . ,_) #t] [`(collects #"typed-racket" . ,_) #t] [_ #f])) (define (context-item->string ci) (match-define (cons id src) ci) (string-append (if (or src id) " " "") (if src (source-location->string src) "") (if (and src id) " " "") (if id (format "~a" id) ""))) ;; Don't use source-location->string from syntax/srcloc. Don't want ;; the setup/path-to-relative behavior that replaces full pathnames ;; with , etc. Instead want full pathnames for Emacs' ;; compilation-mode. HOWEVER note that or might be ;; baked into exn-message string already; we handle that in ;; `fully-qualify-error-path`. Here we handle only strings we create ;; ourselves, such as for the Context "stack trace". (define (source-location->string x) (match-define (srcloc src line col pos span) x) (format "~a:~a:~a" src (or line "1") (or col "1"))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Fully qualified pathnames in error messages, so that Emacs ;; compilation-mode can do its stuff. ;; srcloc->string uses current-directory-for-user to shorten error ;; messages. But we always want full pathnames. Setting it to ;; 'pref-dir -- very unlikely user .rkt file will be there -- is ;; least-worst way AFAIK. (define (show-full-path-in-errors) (current-directory-for-user (find-system-path 'pref-dir))) ;; If this looks like a Racket error message, but the filename is ;; not fully-qualified, prepend curdir to the filename. ;; ;; This covers Racket 5.3.6 and earlier. In fact, this might be ;; sufficient for _all_ versions of Racket and we don't need the ;; `show-full-path-in-errors` thing above, at all. Not yet sure. (define (fully-qualify-error-path s) (match s [(pregexp "^([^:]+):(\\d+)[:.](\\d+)(.*)$" (list _ path line col more)) #:when (not (absolute-path? path)) (string-append (string-join (list (path->string (build-path (current-directory) path)) line col) ":") more)] [s s])) (module+ test (require rackunit) (case (system-type 'os) [(windows) (check-equal? (parameterize ([current-directory "c:\\tmp"]) (fully-qualify-error-path "foo.rkt:3:0: f: unbound identifier\n in: f")) "c:\\tmp\\foo.rkt:3:0: f: unbound identifier\n in: f") (check-equal? (fully-qualify-error-path "c:\\tmp\\foo.rkt:3:0: f: unbound identifier\n in: f") "c:\\tmp\\foo.rkt:3:0: f: unbound identifier\n in: f")] [(macosx unix) (check-equal? (parameterize ([current-directory "/tmp/"]) (fully-qualify-error-path "foo.rkt:3:0: f: unbound identifier\n in: f")) "/tmp/foo.rkt:3:0: f: unbound identifier\n in: f") (check-equal? (fully-qualify-error-path "/tmp/foo.rkt:3:0: f: unbound identifier\n in: f") "/tmp/foo.rkt:3:0: f: unbound identifier\n in: f")]) (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) ""))) (define maybe-suggest-packages (with-handlers ([exn:fail? (λ _ void)]) (with-dynamic-requires ([racket/base exn:missing-module?] [racket/base exn:missing-module-accessor] [pkg/db get-catalogs] [pkg/lib pkg-catalog-suggestions-for-module]) (λ (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 Mananger". 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])])))))) (module+ test ;; Point of this test is older Rackets where the with-handlers ;; clause is exercised. (check-not-exn (λ () (maybe-suggest-packages (exn:fail "" (current-continuation-marks))))))