emacs.d/elpa/racket-mode-20200411.1959/racket/imports.rkt

269 lines
11 KiB
Racket
Raw Normal View History

2020-03-24 18:20:37 +01:00
#lang racket/base
(require racket/contract
racket/format
racket/match
racket/set
"util.rkt")
(provide imports)
;;; Finding completion candidates from imports
;; drracket/check-syntax tells us about local definitions (which is
;; great!), and, tells us about imported definitions -- but only those
;; already _used_. Obviously, a major use case for completion is
;; typing _new_ uses of available definitions, too. e.g. "What is that
;; string-xxx function I'm not yet using in this file?" So we want to
;; supply that full set.
;;
;; If you have a namespace from module->namespace, you can use
;; namespace-mapped-symbols -- easy! However we do NOT want to
;; instantiate the module, i.e. "run the user's code". We want to
;; supply this information using the same sort of "passive" analaysis
;; done by check-syntax, before the user even runs the file (if ever).
;;
;; module->exports is a good starting point, but not the whole answer:
;; Imports can be filtered and renamed -- e.g. only-in, except-in,
;; prefix-in, rename-in.
;;
;; AFAICT there is no good way to get completions from all imported
;; identifiers, except attempting to parse the complete #%require
;; grammar including `prefix` and renaming forms like `just-meta`, and
;; apply that information to tweak the answer from module->exports.
;; It is important to run this with the correct parameterization of
;; current-namespace and current-load-relative-directory.
(define/contract (imports stx [sos (mutable-set)])
(->* (syntax?) (set-mutable?) set-mutable?)
(define (handle-module stx)
(syntax-case stx (module #%module-begin #%plain-module-begin #%require)
[(module _id lang (#%module-begin e ...))
(handle-module-level #'(e ...) #'lang)]
[(module _id lang (#%plain-module-begin e ...))
(handle-module-level #'(e ...) #'lang)]))
(define (handle-module-level es lang)
(module-exported-strings lang lang)
(for ([e (in-syntax es)])
(syntax-case* e (#%require module module*) symbolic-compare?
[(#%require e ...)
(for ([spec (in-syntax #'(e ...))])
(handle-raw-require-spec spec lang))]
[(module _id sub-mod-lang (_mb e ...))
(handle-module-level #'(e ...) #'sub-mod-lang)]
[(module* _id sub-mod-lang (_mb e ...))
(handle-module-level #'(e ...) (if (syntax-e #'sub-mod-lang)
#'sub-mod-lang
lang))]
[ _ (void)])))
(define (handle-raw-require-spec spec lang)
(syntax-case* spec (for-meta for-syntax for-template for-label just-meta) symbolic-compare?
[(for-meta _phase specs ...)
(for ([spec (in-syntax #'(specs ...))])
(handle-phaseless-spec spec lang))]
[(for-syntax specs ...)
(for ([spec (in-syntax #'(specs ...))])
(handle-phaseless-spec spec lang))]
[(for-template specs ...)
(for ([spec (in-syntax #'(specs ...))])
(handle-phaseless-spec spec lang))]
[(for-label specs ...)
(for ([spec (in-syntax #'(specs ...))])
(handle-phaseless-spec spec lang))]
[(just-meta phase specs ...)
(for ([spec (in-syntax #'(specs ...))])
(handle-raw-require-spec spec lang))]
[raw-module-path
(handle-phaseless-spec #'raw-module-path lang)]))
(define (handle-phaseless-spec spec lang)
(syntax-case* spec (only prefix all-except prefix-all-except rename)
symbolic-compare?
[(only _raw-module-path id ...)
(set-union! sos
(syntax->string-set #'(id ...)))]
[(prefix prefix-id raw-module-path)
(module-exported-strings #'raw-module-path
lang
#:prefix #'prefix-id)]
[(all-except raw-module-path id ...)
(module-exported-strings #'raw-module-path
lang
#:except (syntax->string-set #'(id ...)))]
[(prefix-all-except prefix-id raw-module-path id ...)
(module-exported-strings #'raw-module-path
lang
#:prefix #'prefix-id
#:except (syntax->string-set #'(id ...)))]
[(rename raw-module-path local-id exported-id)
(begin
(unless (eq? (syntax-e #'raw-module-path) (syntax-e lang))
(set-remove! sos (->str #'exported-id)))
(set-add! sos (->str #'local-id)))]
[raw-module-path
(module-path? (syntax->datum #'raw-module-path))
(module-exported-strings #'raw-module-path
lang)]))
(define (module-exported-strings raw-module-path
lang
#:except [exceptions (set)]
#:prefix [prefix #'""])
;; NOTE: Important to run this with the correct parameterization of
;; current-namespace and current-load-relative-directory.
(define (add-exports mp)
(define-values (vars stxs) (module->exports mp))
(define orig
(for*/mutable-set ([vars+stxs (in-list (list vars stxs))]
[phases (in-list vars+stxs)]
[export (in-list (cdr phases))])
(->str (car export))))
;; If imports are from the module language, then {except rename
;; prefix}-in do NOT remove imports under the original name.
;; Otherwise they do.
(if (eq? (syntax-e raw-module-path) (syntax-e lang))
(set-union! sos orig)
(set-subtract! sos orig exceptions))
(for ([v (in-set orig)])
(set-add! sos (~a (->str prefix) v))))
;; Ignore non-external module paths: module->exports can't handle
;; them, and anyway, drracket/check-syntax will contribute
;; completion candidates for local definitions, we don't need to
;; find them here.
(syntax-case* raw-module-path (quote submod) symbolic-compare?
[(quote _) sos]
[(submod "." . _) sos]
[_ (add-exports (syntax->datum raw-module-path))]))
(handle-module stx)
sos)
(define (->str v)
(match v
[(? syntax?) (->str (syntax-e v))]
[(? symbol?) (symbol->string v)]
[(? string?) v]))
(define (syntax->string-set s)
(for/mutable-set ([s (in-syntax s)])
(->str s)))
(define (symbolic-compare? x y)
(eq? (syntax-e x) (syntax-e y)))
(module+ completions-example
(parameterize ([current-namespace (make-base-namespace)])
(define stx
(expand
#'(module m racket/base
(module sub racket/base (void))
(require racket/require
(submod "." sub)
(except-in "../error.rkt" show-full-path-in-errors)
(prefix-in XXX: (except-in racket/file other-write-bit))
(rename-in racket/path [path-only PATH-ONLY])))))
(syntax->datum stx)
(imports stx)))
(module+ test
(require rackunit
version/utils)
;; Compare the results to namespace-mapped-symbols.
(module mod racket/base
(module sub racket/base
(define provided-by-submodule 42)
(provide provided-by-submodule))
(require (rename-in racket/path
[path-only PATH-ONLY])
(except-in racket/base println)
(rename-in racket/base
[display DISPLAY])
(prefix-in PREFIX: (only-in racket/base displayln))
(for-syntax (rename-in racket/syntax [format-id FORMAT-ID]))
(submod "." sub))
(define-namespace-anchor nsa)
(define nsms (map symbol->string
(namespace-mapped-symbols
(namespace-anchor->namespace nsa))))
(provide nsms))
(require 'mod)
(define mod/stx
(expand
#`(module mod racket/base
(module sub racket/base
(define provided-by-submodule 42)
(provide provided-by-submodule))
(require (rename-in racket/path
[path-only PATH-ONLY])
(except-in racket/base println)
(rename-in racket/base
[display DISPLAY])
(prefix-in PREFIX: (only-in racket/base displayln))
(for-syntax (rename-in racket/syntax [format-id FORMAT-ID]))
(submod "." sub))
(eprintf "I should not print!"))))
(let (;; The world according to `namespace-mapped-symbols`
[nsms (list->set nsms)]
;; The world according to our `imports`
[cs (parameterize ([current-namespace (make-base-namespace)])
(define stx (expand mod/stx))
(time (imports stx)))])
;; Test {prefix rename except}-in, keeping mind that they work
;; differently for requires that modify the module language
;; imports.
(check-false (set-member? cs "path-only")
"rename-in not from module language hides old name")
(check-true (set-member? cs "PATH-ONLY")
"rename-in not from module language has new name ")
(check-true (set-member? cs "display")
"rename-in from module language does not hide old name")
(check-true (set-member? cs "DISPLAY")
"rename-in from module language has new name")
(check-true (set-member? cs "displayln")
"prefix-in from module language does not hide old name")
(check-true (set-member? cs "PREFIX:displayln")
"prefix-in from module language is available under new name")
;; namespace-mapped-symbols will return some definitions beyond
;; those imported -- it includes {top module}-level bindings. This
;; test accounts for that with a dumb ad hoc list. (More nifty
;; would be to walk our test stx and build that list.)
;;
;; FIXME? Travis CI says this test fails prior to Racket 7.0:
;; namespace-mapped-symbols reports ~400 more symbols --
;; apparently from full racket (should be racket/base). Huh??
;; Well, _our_ results are correct. For now, let's just do the
;; test on Racket 7.0+.
(when (version<=? "7.0" (version))
(check-equal? (set-subtract nsms cs)
(set "tmp.1" "nsms" "nsa" "provided-by-submodule")
"namespace-mapped-symbols returns only a few more, non-imported definitions"))))
(module+ slow-test
;; Exercise our parsing of the #%require grammar: Try doing
;; (check-not-exn (imports stx)) on many files in the Racket
;; distribution. Grammar mistakes will raise exn:fail:syntax.
(require rackunit
racket/path
"syntax.rkt")
(define (check path)
(parameterize ([current-load-relative-directory (path-only path)]
[current-namespace (make-base-namespace)])
(file->expanded-syntax
path
(λ (stx)
(check-not-exn (λ () (imports stx))
(format "#%require grammar handles ~v" path))))))
(for* ([roots (in-list '(("racket.rkt" "typed")
("core.rkt" "typed-racket")
("main.rkt" "racket")))]
[path (in-directory
(path-only
(apply collection-file-path roots)))]
#:when (equal? #"rkt" (filename-extension path)))
(println path)
(check path)))