diff --git a/doc/racket-mode.texi b/doc/racket-mode.texi index d5f1ddde..9c0270be 100644 --- a/doc/racket-mode.texi +++ b/doc/racket-mode.texi @@ -1069,37 +1069,24 @@ Unfold (show) all test submodules. @kbd{M-x} @code{racket-tidy-requires} -Make a single ``require'' form, modules sorted, one per line. +Make a single, sorted ``require'' form. -The scope of this command is the innermost module around point, -including the outermost module for a file using a ``#lang'' line. -All require forms within that module are combined into a single -form. Within that form: +The scope of this command is the innermost module around point -- +whether an explicit submodule form or the outermost module for a +file that has a ``#lang'' line. -@itemize -@item -A single subform is used for each phase level, sorted in this +Merge all require forms within that module to one form. + +Use a single require-spec for each phase-level, sorted in this order: for-syntax, for-template, for-label, for-meta, and plain (phase 0). -@itemize -@item -Within each level subform, the modules are sorted: - -@itemize -@item -Collection path modules -- sorted alphabetically. - -@item -Subforms such as only-in. +Within each phase-level, sort require-specs by module name. -@item -Quoted relative requires -- sorted alphabetically. -@end itemize -@end itemize -@end itemize +Format at most one module per line. -At most one required module is listed per line. +Simplify gratuitous require-specs. For example reduce (only-in m) +to m and elide (combine-in). See also: @ref{racket-trim-requires} and @ref{racket-base-requires}. @@ -1108,16 +1095,28 @@ See also: @ref{racket-trim-requires} and @ref{racket-base-requires}. @kbd{M-x} @code{racket-trim-requires} -Like @ref{racket-tidy-requires} but also deletes unnecessary requires. +Like @ref{racket-tidy-requires} but also delete unnecessary requires. + +Use macro-debugger/analysis/check-requires to analyze. + +The analysis: + +@itemize +@item +Needs the @code{macro-debugger-lib} package. -Note: This only works when the source file can be fully expanded -with no errors. +@item +Only works when the source file can be fully expanded with no +@end itemize +errors. -Note: This only works for requires at the top level of a source -file using #lang. It does NOT work for require forms inside -module forms. Furthermore, it is not smart about module+ or -module* forms -- it might delete top level requires that are -actually needed by such submodules. +@itemize +@item +Only works for requires at the top level of a source file using +@end itemize +#lang -- not for requires inside submodule forms. Furthermore, it +is not smart about module+ or module* forms -- it might delete +outer requires that are actually needed by such submodules. See also: @ref{racket-base-requires}. @@ -1128,24 +1127,16 @@ See also: @ref{racket-base-requires}. Change from ``#lang racket'' to ``#lang racket/base''. -Adds explicit requires for imports that are provided by -``racket'' but not by ``racket/base''. - -This is a recommended optimization for Racket applications. -Avoiding loading all of ``racket'' can reduce load time and -memory footprint. +Using ``racket/base'' is a recommended optimization for Racket +applications. Loading all of ``racket'' is slower and uses more +memory. -Also, as does @ref{racket-trim-requires}, this removes unneeded -modules and tidies everything into a single, sorted require form. +Add explicit requires for imports that are provided by ``racket'' +but not by ``racket/base''. -Note: This only works when the source file can be fully expanded -with no errors. - -Note: This only works for requires at the top level of a source -file using #lang. It does NOT work for require forms inside -module forms. Furthermore, it is not smart about module+ or -module* forms -- it might delete top level requires that are -actually needed by such submodules. +Also do the equivalent of @ref{racket-trim-requires} and +@code{nil}. See those commands for additional notes +and caveats. Note: Currently this only helps change ``#lang racket'' to ``#lang racket/base''. It does not help with other similar @@ -1162,28 +1153,31 @@ Add a require for an identifier. Useful when you know the name of an export but don't remember from what module it is exported. -At the prompt: +1 At the prompt, you may: +@itemize +@item Use @kbd{} or @kbd{} or @kbd{M-n} to load the identifier at point. -You may also need to @kbd{C-e} or @kbd{} to see candidates. - -Or type anything. +@end itemize +You might also need to @kbd{C-e} or @kbd{} to see candidates. -After you choose: +@itemize +@item +Or, type anything. -The identifier you chose is inserted at point if not already -there. +@item +After you choose, this command will: -A ``require'' form is inserted, followed by doing a -@ref{racket-tidy-requires}. +@item +Insert the identifier at point if not already there. -When more than one module supplies an identifer with the same -name, the first is used -- for example ``racket/base'' instead of -``racket''. +@item +Insert a ``require'' form and do @ref{racket-tidy-requires}. +@end itemize -Caveat: This works in terms of identifiers that are documented. -The mechanism is similar to that used for Racket's ``Search -Manuals'' feature. Today there exists no system-wide database of +Caveat: This works only for identifiers that are documented. The +mechanism is similar to that used for Racket's ``Search Manuals'' +feature. Today there exists no system-wide database of identifiers that are exported but not documented. @node racket-indent-line diff --git a/racket-edit.el b/racket-edit.el index 38f241aa..520c0b2d 100644 --- a/racket-edit.el +++ b/racket-edit.el @@ -60,107 +60,77 @@ ;;; requires (defun racket-tidy-requires () - "Make a single \"require\" form, modules sorted, one per line. + "Make a single, sorted \"require\" form for each module. -The scope of this command is the innermost module around point, -including the outermost module for a file using a \"#lang\" line. -All require forms within that module are combined into a single -form. Within that form: +Use a single require-spec for each phase-level, sorted in this +order: for-syntax, for-template, for-label, for-meta, and +plain (phase 0). -- A single subform is used for each phase level, sorted in this - order: for-syntax, for-template, for-label, for-meta, and - plain (phase 0). +Within each phase-level, sort require-specs by module name. - - Within each level subform, the modules are sorted: +Format at most one module per line. - - Collection path modules -- sorted alphabetically. - - - Subforms such as only-in. - - - Quoted relative requires -- sorted alphabetically. - -At most one required module is listed per line. +Simplify gratuitous require-specs. For example reduce (only-in m) +to m and elide (combine-in). See also: `racket-trim-requires' and `racket-base-requires'." (interactive) (racket--assert-sexp-edit-mode) - (racket--tidy-requires '() #'ignore)) - -(defun racket--tidy-requires (add callback) - (pcase (append (racket--module-requires 'find) add) - (`() (user-error "The module has no requires; nothing to do")) - (reqs (racket--cmd/async - nil - `(requires/tidy ,reqs) - (lambda (result) - (pcase result - ("" nil) - (new - (pcase (racket--module-requires 'kill) - (`() - (goto-char (racket--inside-innermost-module)) - (forward-line 1)) - (pos (goto-char pos))) - (let ((pt (point))) - (insert new) - (when (eq (char-before pt) ?\n) - (newline)) - (indent-region pt (1+ (point))) - (goto-char pt)))) - (funcall callback result)))))) + (racket--tidy-requires)) + +(defun racket--tidy-requires (&optional callback) + "Helper for both the `racket-tidy-requires' and +`racket-add-require-for-identifier' commands." + (racket--save-if-changed) + (racket--cmd/async + nil + `(requires/tidy ,(racket--buffer-file-name)) + (lambda (changes) + (racket--require-changes changes) + (when callback + (funcall callback changes))))) (defun racket-trim-requires () - "Like `racket-tidy-requires' but also deletes unnecessary requires. + "Like `racket-tidy-requires' but also delete unnecessary requires. + +Use macro-debugger/analysis/check-requires to analyze. -Note: This only works when the source file can be fully expanded -with no errors. +The analysis: -Note: This only works for requires at the top level of a source -file using #lang. It does NOT work for require forms inside -module forms. Furthermore, it is not smart about module+ or -module* forms -- it might delete top level requires that are -actually needed by such submodules. +- Needs the `macro-debugger-lib` package. + +- Only works when the source file can be fully expanded with no +errors. + +- Only works for requires at the top level of a source file using +#lang -- not for requires inside submodule forms. Furthermore, +the analysis is not smart about module+ or module* forms -- it +might delete outer requires that are actually needed by such +submodules. See also: `racket-base-requires'." (interactive) (racket--assert-edit-mode) (when (racket--submodule-y-or-n-p) (racket--save-if-changed) - (pcase (racket--module-requires 'find t) - (`nil (user-error "The file module has no requires; nothing to do")) - (reqs (racket--cmd/async - nil - `(requires/trim - ,(racket--buffer-file-name) - ,reqs) - (lambda (result) - (pcase result - (`nil (user-error "Syntax error in source file")) - ("" (goto-char (racket--module-requires 'kill t))) - (new (goto-char (racket--module-requires 'kill t)) - (insert (concat new "\n")))))))))) + (racket--cmd/async + nil + `(requires/trim ,(racket--buffer-file-name)) + #'racket--require-changes))) (defun racket-base-requires () "Change from \"#lang racket\" to \"#lang racket/base\". -Adds explicit requires for imports that are provided by -\"racket\" but not by \"racket/base\". - -This is a recommended optimization for Racket applications. -Avoiding loading all of \"racket\" can reduce load time and -memory footprint. - -Also, as does `racket-trim-requires', this removes unneeded -modules and tidies everything into a single, sorted require form. +Using \"racket/base\" is a recommended optimization for Racket +applications. Loading all of \"racket\" is slower and uses more +memory. -Note: This only works when the source file can be fully expanded -with no errors. +Add explicit requires for imports that are provided by \"racket\" +but not by \"racket/base\". -Note: This only works for requires at the top level of a source -file using #lang. It does NOT work for require forms inside -module forms. Furthermore, it is not smart about module+ or -module* forms -- it might delete top level requires that are -actually needed by such submodules. +Also do the equivalent of `racket-trim-requires' and +`racket-tidy-require'. See those commands for additional notes +and caveats. Note: Currently this only helps change \"#lang racket\" to \"#lang racket/base\". It does not help with other similar @@ -174,22 +144,53 @@ typed/racket/base\"." (user-error "File does not use use #lang racket. Cannot change.")) (when (racket--submodule-y-or-n-p) (racket--save-if-changed) - (let ((reqs (racket--module-requires 'find t))) - (racket--cmd/async - nil - `(requires/base - ,(racket--buffer-file-name) - ,reqs) - (lambda (result) - (pcase result - (`nil (user-error "Syntax error in source file")) - (new (goto-char (point-min)) - (re-search-forward "^#lang.*? racket$") - (insert "/base") - (goto-char (or (racket--module-requires 'kill t) - (progn (insert "\n\n") (point)))) - (unless (string= "" new) - (insert (concat new "\n")))))))))) + (racket--cmd/async + nil + `(requires/base ,(racket--buffer-file-name)) + (lambda (changes) + (racket--require-changes changes) + (goto-char (point-min)) + (re-search-forward "^#lang.*? racket$") + (insert "/base"))))) + +(defun racket--require-changes (changes) + "Process response from back end tidy/trim/base commands. + +Each change is either a deletion, a replacement, or an insertion. + +The changes are sorted from greater to smaller positions -- so +that by working backwards through the buffer, we need not worry +about shifting positions of later items. + +The biggest wrinkle here is that, for esthetics, we want to +remove surrounding whitepsace when deleting, or add when +inserting something brand-new. Otherwise, for replacing, it +suffices to make the change and re-indent." + (save-match-data + (dolist (change changes) + (pcase change + (`(delete ,pos ,span) + (delete-region pos (+ pos span)) + (save-excursion + (goto-char pos) + (if (save-excursion + (forward-line 0) + (looking-at "[ \t]*)")) + (delete-indentation) ;i.e. join-line + (delete-blank-lines)))) + (`(replace ,pos ,span ,str) + (delete-region pos (+ pos span)) + (save-excursion + (goto-char pos) + (insert str) + (indent-region pos (point)))) + (`(insert ,pos ,str) + (save-excursion + (goto-char pos) + (newline-and-indent) + (insert str) + (newline-and-indent) + (indent-region pos (point)))))))) (defun racket--submodule-y-or-n-p () (save-excursion @@ -206,40 +207,6 @@ typed/racket/base\"." (re-search-forward re) t))) -(defun racket--module-requires (what &optional outermost-p) - "Identify all require forms and do WHAT. - -When WHAT is \"find\", return the require forms. - -When WHAT is \"kill\", kill the require forms and return the -position where the first one had started. - -OUTERMOST-P says which module's requires: true means the -outermost file module, nil means the innermost module around -point." - (save-excursion - (goto-char (if outermost-p - (point-min) - (racket--inside-innermost-module))) - (let ((first-beg nil) - (requires nil)) - (while - (condition-case _ - (let ((end (progn (forward-sexp 1) (point))) - (beg (progn (forward-sexp -1) (point)))) - (unless (equal end (point-max)) - (when (prog1 (racket--looking-at-require-form) - (goto-char end)) - (unless first-beg (setq first-beg beg)) - (push (read (buffer-substring-no-properties beg end)) - requires) - (when (eq 'kill what) - (delete-region beg end) - (delete-blank-lines))) - t)) - (scan-error nil))) - (if (eq 'kill what) first-beg requires)))) - (defun racket--inside-innermost-module () "Position of the start of the inside of the innermost module around point. This could be \"(point-min)\" if point is within no @@ -251,16 +218,13 @@ module form, meaning the outermost, file module." (while (not (racket--looking-at-module-form)) (backward-up-list)) (down-list) + (forward-line 1) + (re-search-forward "[ \t]*" nil t) (point)) - (scan-error (point-min))))) - -(defun racket--looking-at-require-form () - ;; Assumes you navigated to point using a method that ignores - ;; strings and comments, preferably `forward-sexp'. - (and (eq ?\( (char-syntax (char-after))) - (save-excursion - (down-list 1) - (looking-at-p "require")))) + (scan-error + (goto-char (point-min)) + (or (re-search-forward "^#lang .+?\n$" nil t) + (point-min)))))) (defun racket-add-require-for-identifier () "Add a require for an identifier. @@ -268,44 +232,41 @@ module form, meaning the outermost, file module." Useful when you know the name of an export but don't remember from what module it is exported. -At the prompt: +1 At the prompt, you may: \\ -Use \\[next-history-element] to load the identifier at point. -You may also need to \\[move-end-of-line] to see candidates. +- Use \\[next-history-element] to load the identifier at point. +You might also need to \\[move-end-of-line] to see candidates. -Or type anything. +- Or, type anything. -After you choose: +2. After you choose, this command will: -The identifier you chose is inserted at point if not already -there. +- Insert the identifier at point if not already there. -A \"require\" form is inserted, followed by doing a -`racket-tidy-requires'. +- Insert a \"require\" form and do `racket-tidy-requires'. -When more than one module supplies an identifer with the same -name, the first is used -- for example \"racket/base\" instead of -\"racket\". - -Caveat: This works in terms of identifiers that are documented. -The mechanism is similar to that used for Racket's \"Search -Manuals\" feature. Today there exists no system-wide database of +Caveat: This works only for identifiers that are documented. The +mechanism is similar to that used for Racket's \"Search Manuals\" +feature. Today there exists no system-wide database of identifiers that are exported but not documented." (interactive) (racket--assert-sexp-edit-mode) (when-let (result (racket--describe-search-completing-read)) (pcase-let* ((`(,term ,_path ,_anchor ,lib) result) - (req `(require ,(intern lib)))) + (req (format "(require %s)" lib))) (unless (equal (racket--thing-at-point 'symbol) term) (insert term)) - (let ((pt (copy-marker (point)))) - (racket--tidy-requires - (list req) - (lambda (result) - (goto-char pt) - (when result - (message "Added \"%s\" and did racket-tidy-requires" req)))))))) + (save-excursion + (goto-char (racket--inside-innermost-module)) + (insert req) + (newline-and-indent) + (let ((pt (copy-marker (point)))) + (racket--tidy-requires + (lambda (result) + (goto-char pt) + (when result + (message "Added %S and did racket-tidy-requires" req))))))))) ;;; align diff --git a/racket/command-server.rkt b/racket/command-server.rkt index 42b6cbce..ea4eab7c 100644 --- a/racket/command-server.rkt +++ b/racket/command-server.rkt @@ -140,9 +140,9 @@ [`(macro-stepper ,path ,str ,pol) (macro-stepper path str pol)] [`(macro-stepper/next ,what) (macro-stepper/next what)] [`(module-names) (module-names)] - [`(requires/tidy ,reqs) (requires/tidy reqs)] - [`(requires/trim ,path-str ,reqs) (requires/trim path-str reqs)] - [`(requires/base ,path-str ,reqs) (requires/base path-str reqs)] + [`(requires/tidy ,path-str) (requires/tidy path-str)] + [`(requires/trim ,path-str) (requires/trim path-str)] + [`(requires/base ,path-str) (requires/base path-str)] [`(doc-search ,prefix) (doc-search prefix)] [`(hash-lang . ,more) (apply hash-lang more)] [`(pkg-list) (package-list)] diff --git a/racket/commands/requires.rkt b/racket/commands/requires.rkt index e71ffb5b..9c59becb 100644 --- a/racket/commands/requires.rkt +++ b/racket/commands/requires.rkt @@ -1,247 +1,561 @@ -;; Copyright (c) 2013-2022 by Greg Hendershott. +;; Copyright (c) 2013-2025 by Greg Hendershott. ;; SPDX-License-Identifier: GPL-3.0-or-later #lang at-exp racket/base -(require racket/contract - racket/format - racket/function - (only-in racket/list append* append-map add-between filter-map) +(require (only-in racket/list + add-between + append* + argmin + cartesian-product + remove-duplicates) racket/match - racket/set - racket/string - "../util.rkt") + (only-in racket/path + simple-form-path) + syntax/parse/pre + (only-in syntax/stx + stx-map) + (only-in "../syntax.rkt" + file->syntax) + (only-in "../util.rkt" + safe-dynamic-require)) (provide requires/tidy requires/trim requires/base) (module+ test - (require rackunit)) + (require rackunit + racket/format + version/utils)) -(define require-subform? (or/c module-path? list?)) -(define require-form? (cons/c 'require (listof require-subform?))) +(define (requires/tidy path-str) + (tidy-file path-str)) -(define level? (or/c #f number? 'racket/require)) -(define denormalized? (hash/c level? (set/c require-subform?))) +(define (requires/trim path-str) + (tidy-file path-str + #:drops (drops (analyze path-str)))) - -(define/contract (requires/tidy reqs) - (-> (listof require-form?) string?) - (require-pretty-format - (normalize - (denormalize reqs)))) - -(module+ test - (check-equal? - (requires/tidy '((require z) - (require (prefix-in a: a)) - (require c d e))) - "(require (prefix-in a: a)\n c\n d\n e\n z)\n")) - -;; Note: Why pass in a list of the existing require forms -- why not -;; just use the "keep" list from show-requires? Because the keep list -;; only states the module name, not the original form. Therefore if -;; the original require has a subform like `(only-in mod f)` (or -;; rename-in, except-in, &c), we won't know how to preserve that -;; unless we're given it. That's why our strategy must be to look for -;; things to drop, as opposed to things to keep. -(define/contract (requires/trim path-str reqs) - (-> path-string? (listof require-form?) string?) - (require-pretty-format - (normalize - (denormalize reqs - #:drops (requires-to-drop (analyze path-str)))))) - -(define/contract (requires/base path-str reqs) - (-> path-string? (listof require-form?) string?) +(define (requires/base path-str) (define a (analyze path-str)) - (require-pretty-format - (normalize - (denormalize reqs - #:adds (requires-to-add a) - #:drops (requires-to-drop a))))) + (tidy-file path-str + #:adds (adds/racket->racket/base a) + #:drops (drops a))) -;;; analyze - -(define requires-analysis? (listof (or/c (list/c 'keep module-path? number?) - (list/c 'bypass module-path? number? list?) - (list/c 'drop module-path? number?)))) -(define mod+level? (list/c module-path? number?)) +(module+ test + (let ([p (path->string + (simple-form-path + (build-path 'up 'up "test" "example" "requires.rkt")))]) + (check-equal? + (requires/tidy p) + '((delete 174 17) + (replace + 129 + 44 + "(require net/url\n (combine-in net/url\n racket/format))") + (replace 39 42 "(require net/uri-codec\n net/url)"))) + + ;; On older Rackets macro-debugger/analysis/check-requires might + ;; give error: + ;; + ;; derivation-parser: error on token #2: > + (when (version<=? "8.14" (version)) + (check-equal? + (requires/trim p) + '((delete 174 17) + (replace + 129 + 44 + "(require net/url\n (combine-in net/url\n racket/format))") + (replace 39 42 "(require net/uri-codec\n net/url)"))) + (check-equal? + (requires/base p) + '((delete 174 17) + (replace + 129 + 44 + "(require (for-syntax racket/base)\n net/url\n (combine-in net/url\n racket/format)\n racket/list\n racket/match)") + (replace 39 42 "(require net/uri-codec\n net/url)")))))) + +;;; analysis by macro-debugger/analysis/check-requires + +(define (error-thunk) + (error 'requires + "Cannot work until you `raco pkg install macro-debugger-lib`")) (define show-requires (safe-dynamic-require 'macro-debugger/analysis/check-requires 'show-requires - (λ () (error 'requires "Won't work until you `raco pkg install macro-debugger-lib`")))) + error-thunk)) -(define/contract (analyze path-str) - (-> path-string? requires-analysis?) +(define (analyze path-str) (define-values (base name _) (split-path (string->path path-str))) (parameterize ([current-load-relative-directory base] [current-directory base]) (show-requires name))) -;; Use `bypass` convert from `#lang racket` to `#lang racket/base` -;; plus explicit requires. Hardcoded to `#lang racket`, only. -(define/contract (requires-to-drop a) - (-> requires-analysis? (listof mod+level?)) - (filter-map (λ (x) - (match x - [(list 'drop mod lvl) (list mod lvl)] - [_ #f])) - a)) - -(define/contract (requires-to-add a) - (-> requires-analysis? (listof mod+level?)) - (append* - (filter-map (λ (x) - (match x - [(list 'bypass 'racket 0 (list (list mod lvl _) ...)) - (filter (λ (x) - (match x - [(list 'racket/base 0) #f] - [_ #t])) - (map list mod lvl))] - [_ #f])) - a))) - -;;; denormalize / normalize - -(define/contract (denormalize reqs - #:drops [drops '()] - #:adds [adds '()]) - (->* ((listof require-form?)) - (#:adds (listof mod+level?) - #:drops (listof mod+level?)) - denormalized?) - (define ht (make-hasheq)) - (define (add* level v) - (unless (and (not (eq? v 'racket/require)) ;always keep - (member (list (form-mod v) level) drops)) - (hash-update! ht - (if (eq? v 'racket/require) 'racket/require level) - (λ (s) (set-add s v)) - set))) - (define (add level v) - (match v - [(list* 'multi-in vs) (for-each (curry add* level) (multi vs))] - [v (add* level v)])) - (for ([add (in-list adds)]) - (match-define (list mod level) add) - (add* level mod)) - (for ([req (in-list reqs)]) - (match-define (cons 'require vs) req) - (for ([v (in-list vs)]) - (match v - [(list* 'for-meta level vs) (for-each (curry add level) vs)] - [(list* 'for-syntax vs) (for-each (curry add 1 ) vs)] - [(list* 'for-template vs) (for-each (curry add -1 ) vs)] - [(list* 'for-label vs) (for-each (curry add #f ) vs)] - [v (add 0 v)]))) - ht) - -;; `multi` from racket/require adapted for plain sexprs not stxs -(define (multi xs) - (define (loop xs) - (if (null? xs) - '(()) - (let ([first (car xs)] - [rest (loop (cdr xs))]) - (if (list? first) - (let ([bads (filter list? first)]) - (if (null? bads) - (append-map (λ (x) (map (λ (y) (cons x y)) rest)) first) - (error 'multi-in "not a simple element" (car bads)))) - (map (λ (x) (cons first x)) rest))))) - (define options (loop xs)) - (define (try pred? ->str str->) - (and (andmap (λ (x) (andmap pred? x)) options) - (map (λ (x) - (let* ([d x] - [r (apply string-append - (add-between (if ->str (map ->str d) d) - "/"))]) - (if str-> (str-> r) r))) - options))) - (or (try string? #f #f) - (try symbol? symbol->string string->symbol) - (error 'multi-in "only accepts all strings or all symbols"))) - -(module+ test - (let ([ht (denormalize '((require a b c) - (require d e) - (require a f) - (require - (for-syntax s t u) - (for-label l0 l1 l2)) - (require - (for-meta 1 m1a m1b) - (for-meta 2 m2a m2b)) - (require - (multi-in foo (bar baz)) - (multi-in "foo" ("bar.rkt" "baz.rkt")))))]) - (check-equal? (hash-ref ht 0) - (set 'a 'e 'd 'foo/bar "foo/baz.rkt" "foo/bar.rkt" 'c 'f 'b 'foo/baz)) - (check-equal? (hash-ref ht 1) - (set 'm1a 'm1b 't 'u 's)) - (check-equal? (hash-ref ht 2) - (set 'm2b 'm2a)) - (check-equal? (hash-ref ht #f) - (set 'l1 'l2 'l0)))) - -;; Sort the subforms by phase level: for-syntax, for-template, -;; for-label, for-meta, and plain (0). Within each such group, sort -;; them first by module paths then relative requires. Within each such -;; group, sort alphabetically. If racket/require is present, sort it -;; first and use multi-in. -(define/contract (normalize ht) - (-> denormalized? require-form?) - (define (mod-set->mod-list mod-set) - (sort (set->list mod-set) modracket/base analyses) + (for*/list ([a (in-list analyses)] + [replacements (in-value + (match a + [(list 'bypass 'racket _level rs) + (cons '(racket/base 1 #f) + rs)] + [_ null]))] + [replacement (in-list replacements)] + [v (in-value + (match replacement + ;; Don't need racket/base when that will be the + ;; #lang, so ignore. + [(list 'racket/base 0 _) #f] + ;; We're ignoring the "rename" field because it's + ;; N/A for our specfic scenario, changing racket + ;; to racket/base. + [(list mod level _) (cons mod level)]))] + #:when v) + v)) + +;;; tidy-module + +(define current-phase-level (make-parameter 0)) +(define current-drops (make-parameter null)) ; (listof (cons mod level)) + +;; The `sortable` (names are hard) struct represents the syntax for a +;; require-spec, along with information about how to sort it. +;; +;; The `group` is a number, used to order things like absolute path +;; symbols above relative path strings. +;; +;; The `key` is a symbol or string, used to sort alphabetically within +;; each group. +;; +;; The sortable-group+key function produces a string comprising both, +;; suitable to give to `sort` as the #:key, with of course `stringsortable sort-group modpath-stx) + (define mod (syntax->datum modpath-stx)) + (and (not (member (cons mod (current-phase-level)) + (current-drops))) + (sortable modpath-stx sort-group mod))) + +(define (copy orig stx) + (and orig + (sortable stx + (sortable-group orig) + (sortable-key orig)))) + +(define (combine sortables stx-preface) + (match (sort (filter values sortables) ;non-false + #:key sortable-group+key + stringsortable group-id #'id)) + (pattern (lib) ;elide + #:attr s #f) + (pattern (lib str:string ...+) + #:attr s + (combine (stx-map (λ (str) (modpath->sortable group-lib str)) + #'(str ...)) + #'(lib))) + (pattern (file str:string) + #:attr s (modpath->sortable group-file #'str)) + (pattern str:string + #:attr s (modpath->sortable group-string #'str)) + (pattern (quote id:id) + #:attr s (modpath->sortable group-quote #'(quote id)))) + +(define-syntax-class submod-path-element + (pattern _id:id) + (pattern "..")) + +(define-syntax-class module-path + #:attributes (s) + #:datum-literals (submod) + (pattern rmp:root-module-path + #:attr s (attribute rmp.s)) + (pattern (submod rmp:root-module-path _:submod-path-element ...+) + #:attr s (attribute rmp.s)) + (pattern (submod (~or "." "..") _:submod-path-element ...+) + #:attr s (sortable this-syntax group-rel-submod "."))) + +(define-syntax-class require-spec + #:attributes (s) + #:datum-literals (;racket/base + only-in except-in prefix-in rename-in combine-in + relative-in only-meta-in only-space-in for-space + ;racket/require + matching-identifiers-in subtract-in filtered-in + path-up multi-in) + #:local-conventions ([spec require-spec]) + + ;; Main jobs here: + ;; + ;; 1. Calculate a `sortable` as the value for the `s` attribute. For + ;; "complex" specs this uses the sort info from one of its + ;; sub-specs; see `copy` and `combine`. + ;; + ;; 2. Simplify specs. For example `(only-in m)` -- with no + ;; exceptions listed -- is legal but can be reduced to just `m`. + ;; Furthermore, `(only-in m)` and `m` both elide to nothing when `m` + ;; is a member of `current-drops`, in which case the `a` attribute + ;; is #f. Likewise (combine-in) elides to #f. + + (pattern mp:module-path + #:attr s (attribute mp.s)) + + (pattern (only-in spec) ;reduce + #:attr s (attribute spec.s)) + (pattern (only-in spec _id-maybe-renamed ...+) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (except-in spec) ;reduce + #:attr s (attribute spec.s)) + (pattern (except-in spec _id ...+) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (prefix-in _prefix:id spec) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (rename-in spec) ;reduce + #:attr s (attribute spec.s)) + (pattern (rename-in spec [_orig:id _bind:id] ...) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (relative-in _mp:module-path) ;elide + #:attr s #f) + (pattern (relative-in mp:module-path spec ...+) + #:attr s (combine (attribute spec.s) + #'(relative-in mp))) + + (pattern (combine-in spec ...) + #:attr s (combine (attribute spec.s) + #'(combine-in))) + + (pattern (only-meta-in phase spec ...) + #:attr s (combine (attribute spec.s) + #`(only-meta-in phase))) + + (pattern (only-space-in space spec ...) + #:attr s (combine (attribute spec.s) + #`(only-space-in space))) + + (pattern (for-space space spec ...) + #:attr s (combine (attribute spec.s) + #`(for-space space))) + + (pattern (matching-identifiers-in _regexp spec) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (subtract-in spec) ;reduce + #:attr s (attribute spec.s)) + (pattern (subtract-in spec _subtracted-spec ...) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (filtered-in _proc spec) + #:attr s (copy (attribute spec.s) this-syntax)) + + (pattern (path-up) ;elide + #:attr s #f) + (pattern (path-up str:string ...+) + #:attr s + (combine (stx-map (λ (str) (modpath->sortable 1 str)) + #'(str ...)) + #'(path-up))) + + (pattern (multi-in) ;elide + #:attr s #f) + + (pattern (multi-in id:id ...+) ;reduce to id/id .. + #:attr s + (modpath->sortable group-id + #`#,(join (syntax->datum #'(id ...)) + symbol->string + string->symbol))) + (pattern (multi-in str:string ...+) ;reduce to str/str .. + #:attr s + (modpath->sortable group-string + #`#,(join (syntax->datum #'(str ...))))) + + (pattern (multi-in id:id ...+ p:multi-in-id-subs) ;1xN + #:attr s + (multi-in-1xN (syntax->datum #'(id ...)) + (attribute p.ids) + symbol->string + string->symbol)) + (pattern (multi-in str:string ...+ p:multi-in-string-subs) ;1xN + #:attr s + (multi-in-1xN (syntax->datum #'(str ...)) + (attribute p.strs))) + + (pattern (multi-in p:multi-in-id-subs ...+) + #:attr s (multi-in-general (attribute p.ids) + symbol->string + string->symbol)) + (pattern (multi-in p:multi-in-string-subs ...+) + #:attr s (multi-in-general (attribute p.strs)))) + +(define-syntax-class multi-in-string-subs + #:attributes (strs) + (pattern str:string + #:attr strs (syntax->datum #'(str))) + (pattern (str:string ...+) + #:attr strs (syntax->datum #'(str ...)))) + +(define-syntax-class multi-in-id-subs + #:attributes (ids) + (pattern id:id + #:attr ids (syntax->datum #'(id))) + (pattern (id:id ...+) + #:attr ids (syntax->datum #'(id ...)))) + +(define (join vs [->str values] [str-> values]) + (str-> + (apply string-append + (add-between (map ->str vs) + "/")))) + +;; The simple case of one or more single subs and a final list of subs +;; -- e.g. (multi-in typed racket (string format)) -- which allows us +;; to handle dropped modules by removing an item from the final subs. +;; Or, if all final subs removed, elide the multi-in form entirely. +(define (multi-in-1xN single-subs final-subs [->str values] [str-> values]) + (define sorted (sort final-subs #:key ->str stringstr str->))] + #:unless (member (cons mod (current-phase-level)) + (current-drops))) + p)) + (cond + [(null? filtered) #f] ;nothing remains, elide entirely + [else + (sortable #`(multi-in #,@single-subs + #,(simplify-subs filtered)) + (if (equal? ->str values) group-string group-id) + (join (append single-subs (list (car final-subs))) + ->str str->))])) + +;; The general case of any cartesian product. +;; +;; Expects every subs to be normalized to a list, even if it contains +;; just one item. +(define (multi-in-general subs [->str values] [str-> values]) + (define sorted (map (λ (sub-path) + (sort sub-path #:key ->str stringstr str->)) + (define sort-group (if (string? sort-key) group-string group-id)) + (define products (apply cartesian-product sorted)) + (define mods (for/list ([vs (in-list products)]) + (join vs ->str str->))) + (define drops (for*/list ([mod (in-list mods)] + #:when (member (cons mod (current-phase-level)) + (current-drops))) + mod)) + (cond + ;; Trivial case: As-is, just simplify. + [(null? drops) + (sortable #`(multi-in #,@(simplify-subs sorted)) + sort-group + sort-key)] + [else + ;; General case for any cartesian product: IIUC the best we can + ;; do is subtract, i.e. wrap the multi-in in subtract-in. + ;; + ;; Example: (multi-in (a b) (0 1) (x y)) + ;; + ;; If we're to drop only b/1/y -- but keep a/1/y -- how else + ;; could we rewrite this? + (sortable #`(subtract-in (multi-in #,@(simplify-subs sorted)) + #,@drops) + sort-group + sort-key)])) + +(define (simplify-subs subs) + (for/list ([s (in-list subs)]) + (match s + [(list v) v] + [v v]))) + +(module+ example + (syntax-parse #'(multi-in "math" "matrix" "utils.rkt") + [spec:require-spec (attribute spec.s)]) + (syntax-parse #'(multi-in "math" ("matrix.rkt" "foo.rkt")) + [spec:require-spec (attribute spec.s)]) + (parameterize ([current-drops (list (cons 'racket/format 0))]) + (syntax-parse #'(multi-in racket (string format match)) + [spec:require-spec (attribute spec.s)])) + (parameterize ([current-drops (list (cons 'racket/format 0))]) + (syntax-parse #'(multi-in racket (string format)) + [spec:require-spec (attribute spec.s)])) + (parameterize ([current-drops (list (cons 'racket/string 0))]) + (syntax-parse #'(multi-in (foobar racket) (string format)) + [spec:require-spec (attribute spec.s)]))) + +(define-syntax-class phased-require-spec + #:attributes (phase specs) + #:datum-literals (racket/require for-syntax for-template for-label for-meta) + #:local-conventions ([spec require-spec]) + (pattern racket/require + #:attr phase 'racket/require + #:with specs #'(racket/require)) + ;; Note: We don't attempt to recognize nested phased specs such as + ;; (for-syntax (for-template _)) and reduce to a net phase level. + ;; Although such forms are legal, and might be generated by macros, + ;; instead our goal here is to parse specs that a human is likely to + ;; write by hand. + (pattern (for-syntax spec ...) + #:attr phase 1 + #:with specs #'(spec ...)) + (pattern (for-template spec ...) + #:attr phase -1 + #:with specs #'(spec ...)) + (pattern (for-label spec ...) + #:attr phase #f + #:with specs #'(spec ...)) + (pattern (for-meta phase-level spec ...) + #:attr phase (syntax->datum #'phase-level) + #:with specs #'(spec ...)) + (pattern spec + #:attr phase 0 + #:with specs #'(spec))) + +(define (tidy-module reqs #:adds [adds null] #:drops [drops null]) + (define ht (make-hash)) ;phase-level => sortable? + (define (add! v) + (hash-update! ht + (current-phase-level) + (λ (vs) (cons v vs)) + null)) + ;; Denormalize + (syntax-parse reqs + #:datum-literals (require) + [((require spec:phased-require-spec ...) ...) + (for ([spec (in-list (syntax->list #'(spec ... ...)))]) + (syntax-parse spec + [v:phased-require-spec + (parameterize ([current-phase-level (attribute v.phase)] + [current-drops drops]) + (for ([spec (in-list (syntax->list #'v.specs))]) + (syntax-parse spec + [spec:require-spec + (define v (attribute spec.s)) + (when v (add! v))])))]))]) + (for ([mod+level (in-list adds)]) + (match-define (cons mod level) mod+level) + (parameterize ([current-phase-level level]) + (define group (if (string? mod) group-string group-id)) + (define v (modpath->sortable group #`#,mod)) + (when v (add! v)))) + ;; Normalize (define (for-level level k) - (match (hash-ref ht level #f) - [#f '()] - [mods - #:when (and (not (eq? level 'racket/require)) - (hash-ref ht 'racket/require #f)) - (k (add-multi-in (mod-set->mod-list mods)))] - [mods - (k (mod-set->mod-list mods))])) - (define (preface . pres) - (λ (mods) `((,@pres ,@mods)))) - (define (meta-levels) - (sort (for/list ([x (hash-keys ht)] - #:when (not (member x '(racket/require -1 0 1 #f)))) - x) + (define sorted (sort (hash-ref ht level null) + #:key sortable-group+key + stringdatum (sortable-stx v))))) + (cond [(null? datums) null] + [else (k datums)])) + (define ((preface . pres) specs) + `((,@pres ,@specs))) + (define meta-levels + (sort (filter (match-lambda + [(or -1 0 1) #f] + [(? number?) #t] + [_ #f]) + (hash-keys ht)) <)) - `(require - ,@(for-level 'racket/require values) - ,@(for-level 1 (preface 'for-syntax)) - ,@(for-level -1 (preface 'for-template)) - ,@(for-level #f (preface 'for-label)) - ,@(append* (for/list ([level (in-list (meta-levels))]) - (for-level level (preface 'for-meta level)))) - ,@(for-level 0 values))) + (match `(require + ,@(for-level 'racket/require values) + ,@(for-level 1 (preface 'for-syntax)) + ,@(for-level -1 (preface 'for-template)) + ,@(for-level #f (preface 'for-label)) + ,@(append* (for/list ([level (in-list meta-levels)]) + (for-level level (preface 'for-meta level)))) + ,@(for-level 0 values)) + ['(require) #f] + [v v])) (module+ test - ;; with racket/require - (check-equal? (normalize - (denormalize - '((require z c b a) - (require racket/require) ; <==== - (require (multi-in mi-z (mi-z0 mi-z1))) - (require mi-z/mi-z2) - (require (multi-in mi-a (mi-a1 mi-a0))) - (require mi-a/mi-a2) - (require (for-meta 4 m41 m40)) - (require (for-meta -4 m-41 m-40)) - (require (for-label l1 l0)) - (require (for-template t1 t0)) - (require (for-syntax s1 s0)) - (require - "a.rkt" "b.rkt" "c.rkt" "z.rkt" - (only-in "mod.rkt" oi) - (only-in mod oi))))) + (check-equal? (tidy-module + #:drops (list (cons 'racket/string 0) + (cons 'bar/b 0)) + '((require z c b a) + (require racket/require) + (require (multi-in mi-z (mi-z0 mi-z1))) + (require (multi-in mi-a (mi-a1 mi-a0))) + (require (multi-in baz (a b))) + (require (multi-in racket (format string match))) + (require (multi-in racket string)) ;disappears + (require (multi-in racket list)) ;=> racket/list + (require (multi-in "dir" "file.rkt")) ;=> "dir/file.rkt" + (require (multi-in (foo bar) (a b c))) + (require (combine-in)) ;disappears + (require (combine-in combine-0)) ;=> combine-0 + (require (combine-in racket/string)) ;disappears + (require (combine-in combine-0 racket/string)) ;=> combine-0 + (require (for-meta 4 m41 m40)) + (require (for-meta -4 m-41 m-40)) + (require (for-label l1 l0)) + (require (for-template t1 t0)) + (require (for-syntax s1 s0)) + (require + "a.rkt" "b.rkt" "c.rkt" "z.rkt" + (only-in "mod.rkt" oi) + (only-in mod oi)))) '(require racket/require (for-syntax s0 s1) @@ -249,219 +563,227 @@ (for-label l0 l1) (for-meta -4 m-40 m-41) (for-meta 4 m40 m41) - a b c - (multi-in mi-a (mi-a0 mi-a1 mi-a2)) ;b/c racket/require - (multi-in mi-z (mi-z0 mi-z1 mi-z2)) ;b/c racket/require - (only-in mod oi) z - "a.rkt" "b.rkt" "c.rkt" (only-in "mod.rkt" oi) "z.rkt")) - ;; without racket/require - (check-equal? (normalize - (denormalize - '((require z c b a) - (require mi-a/mi-a0) - (require mi-a/mi-a1) - (require mi-z/mi-z0) - (require mi-z/mi-z1) - (require (for-meta 4 m41 m40)) - (require (for-meta -4 m-41 m-40)) - (require (for-label l1 l0)) - (require (for-template t1 t0)) - (require (for-syntax s1 s0)) - (require - "a.rkt" "b.rkt" "c.rkt" "z.rkt" - (only-in "mod.rkt" oi) - (only-in mod oi))))) - '(require - (for-syntax s0 s1) - (for-template t0 t1) - (for-label l0 l1) - (for-meta -4 m-40 m-41) - (for-meta 4 m40 m41) - a b c - mi-a/mi-a0 - mi-a/mi-a1 - mi-z/mi-z0 - mi-z/mi-z1 - (only-in mod oi) z - "a.rkt" "b.rkt" "c.rkt" (only-in "mod.rkt" oi) "z.rkt"))) - -(define (add-multi-in xs) - ;; (-> (listof require-subform?) (listof require-subform?)) - ;; 1. Assumes xs are sorted. 2. Only tries to discover/add multi-in - ;; forms where the first element is a single item -- e.g. (multi-in - ;; a (b c)) but not (multi-in (a b) (c d)). - (define (split v) - (cond [(string? v) (string-split v #px"/")] - [(symbol? v) (map string->symbol (string-split (symbol->string v) #px"/"))] - [else (list)])) - (define (join vs) - (cond [(andmap string? vs) (string-join vs "/")] - [(andmap symbol? vs) (string->symbol (string-join (map symbol->string vs) "/"))] - [else (error 'add-multi-in "not strings or symbols")])) - (let loop ([xs xs]) - (match xs - [(list) (list)] - [(list x) (list x)] - [(list* (and `(multi-in ,pre ,vs) this) next more) - (define-values (pres this-rest next-rest) (split-common-prefix (split pre) (split next))) - (cond [(equal? (split pre) pres) - (loop (list* `(multi-in ,pre ,(append vs (list (join next-rest)))) - more))] - [else - (cons this (loop (list* next more)))])] - [(list* this next more) - (define-values (pres this-rest next-rest) (split-common-prefix (split this) (split next))) - (cond [(null? pres) - (cons this (loop (list* next more)))] - [else - (loop (list* `(multi-in ,(join pres) (,(join this-rest) ,(join next-rest))) - more))])]))) - -(module+ test - (check-equal? (add-multi-in '(a b c)) - '(a b c)) - (check-equal? (add-multi-in '(a (prefix-in b: b) c)) - '(a (prefix-in b: b) c)) - (check-equal? (add-multi-in '(racket/string b c)) - '(racket/string b c)) - (check-equal? (add-multi-in '(racket/format racket/string s t)) - '((multi-in racket (format string)) s t)) - (check-equal? (add-multi-in '(racket/contract racket/format racket/string s t)) - '((multi-in racket (contract format string)) s t)) - (check-equal? (add-multi-in '(a/b/x a/b/y)) - '((multi-in a/b (x y)))) - (check-equal? (add-multi-in '("a.rkt" "b.rkt" "c.rkt")) - '("a.rkt" "b.rkt" "c.rkt")) - (check-equal? (add-multi-in '("a.rkt" (prefix-in b: "b.rkt") "c.rkt")) - '("a.rkt" (prefix-in b: "b.rkt") "c.rkt")) - (check-equal? (add-multi-in '("a/x.rkt" "b.rkt" "c.rkt")) - '("a/x.rkt" "b.rkt" "c.rkt")) - (check-equal? (add-multi-in '("a/x.rkt" "a/y.rkt" "b.rkt" "c.rkt")) - '((multi-in "a" ("x.rkt" "y.rkt")) "b.rkt" "c.rkt")) - (check-equal? (add-multi-in '("a/x.rkt" "a/y.rkt" "a/z.rkt" "b.rkt" "c.rkt")) - '((multi-in "a" ("x.rkt" "y.rkt" "z.rkt")) "b.rkt" "c.rkt")) - (check-equal? (add-multi-in '("a/b/x.rkt" "a/b/y.rkt")) - '((multi-in "a/b" ("x.rkt" "y.rkt"))))) - -;; Defined here b/c not in Racket < 6.3 and we support 6.2 -(define (split-common-prefix as bs) - (let loop ([as as] [bs bs]) - (if (and (pair? as) (pair? bs) (equal? (car as) (car bs))) - (let-values ([(prefix atail btail) (loop (cdr as) (cdr bs))]) - (values (cons (car as) prefix) atail btail)) - (values null as bs)))) - -(define (form-mod x) - (match x - [(list 'only-in m _ ...) (form-mod m)] - [(list 'except-in m _ ...) (form-mod m)] - [(list 'prefix-in _ m) (form-mod m)] - [(list 'relative-in _ m _ ...) (form-mod m)] - [(list 'multi-in m _) (form-mod m)] - [m m])) - -(define (modstring a) (symbol->string b)))))) - -(module+ test - (check-true (mod list? string?) + a + b + (subtract-in ;drop bar/b but keep foo/b + (multi-in (bar foo) (a b c)) + bar/b) + (multi-in baz (a b)) + c + combine-0 + (multi-in mi-a (mi-a0 mi-a1)) + (multi-in mi-z (mi-z0 mi-z1)) + (only-in mod oi) + (multi-in racket (format match)) ;racket/string dropped + racket/list + z + "a.rkt" "b.rkt" "c.rkt" + "dir/file.rkt" + (only-in "mod.rkt" oi) + "z.rkt"))) + +;;; tidy-file + +;; Per each module (file root-module or submodule): (listof syntax?). +(define current-module-old-requires (make-parameter null)) + +;; Per entire file: (listof (or/c insert? delete?)). +(struct delete (pos span) #:transparent) +(struct insert (pos str) #:transparent) +(define current-file-changes (make-parameter null)) + +;; cons a new value onto a list-valued parameter +(define (cons-param v p) + (p (cons v (p)))) + +(define-syntax-class require-statement + #:datum-literals (require) + (pattern (require _:phased-require-spec ...) + #:do + [(cons-param this-syntax + current-module-old-requires) + (cons-param (delete (syntax-position this-syntax) + (syntax-span this-syntax)) + current-file-changes)])) + +(define-syntax-class module-level + (pattern _:require-statement) + (pattern _:submodule-statement) + (pattern _)) + +(define-syntax-class submodule-statement + #:datum-literals (module) + (pattern (module _mod _lang ml ...) + #:do + [(parameterize ([current-module-old-requires null]) + (syntax-parse #'(ml ...) + [(_:module-level ...) + ;; Intentionally not supplying adds/drops because + ;; analysis doesn't work for submodules. + (add-insert-change-for-new-require)]))])) + +(define-syntax-class root-module + #:attributes (lang) + #:datum-literals (module #%module-begin) + (pattern (module _mod lang (#%module-begin _:module-level ...)))) + +(define (tidy-file path-str #:adds [adds null] #:drops [drops null]) + (parameterize ([current-file-changes null]) + (syntax-parse (file->syntax path-str) + [rm:root-module + (add-insert-change-for-new-require #:adds adds + #:drops drops + #:lang #'rm.lang) + ;; Sort changes in reverse position, with deletes sorting + ;; before inserts at the same position. + (define changes (sort (current-file-changes) + > + #:key + (match-lambda + [(insert pos _) pos] + [(delete pos _) (+ pos 0.5)]))) + ;; Return a list of changes. A consecutive delete and insert at + ;; same position is consolidated into (list 'replace pos span + ;; str). The remaining changes are converted to (list 'delete + ;; pos span) or (list 'insert pos str). + (let loop ([changes changes]) + (match changes + [(list) (list)] + [(list* (delete pos span) (insert pos str) more) + (cons (list 'replace pos span str) (loop more))] + [(cons (delete pos span) more) + (cons (list 'delete pos span) (loop more))] + [(cons (insert pos str) more) + (cons (list 'insert pos str) (loop more))]))]))) + +;; Given current-module-old-requires, as well as optional adds and +;; drops, tidy into a single new require statement. Add that `insert` +;; change to current-file-changes. +(define (add-insert-change-for-new-require #:adds [adds null] + #:drops [drops null] + #:lang [lang #f]) + (define olds (current-module-old-requires)) + (define new (tidy-module (sort olds < #:key syntax-position) + #:adds adds + #:drops drops)) + (when new + ;; Preferably we insert the new require at the same position as + ;; the first of the old requires. However if no old require (as + ;; can happen changing #lang racket -> racket/base) position it + ;; after the #lang line. + (define pos + (cond + [(null? olds) + (unless lang + (error 'tidy-file + "No old require position and unknown lang syntax")) + (+ (syntax-position lang) (syntax-span lang) 1)] + [else + (syntax-position (argmin syntax-position olds))])) + (cons-param (insert pos (format-require new)) + current-file-changes))) + +;;; format + +(define (format-require x) (define out (open-output-string)) (parameterize ([current-output-port out]) - (require-pretty-print x)) + (print-require x)) (get-output-string out)) (module+ test - (check-equal? (require-pretty-format + (check-equal? (format-require '(require a)) - @~a{(require a) - - }) - (check-equal? (require-pretty-format + @~a{(require a)}) + (check-equal? (format-require '(require a b)) @~a{(require a - b) - - }) - (check-equal? (require-pretty-format + b)}) + (check-equal? (format-require '(require (for-syntax a b) (for-meta 2 c d) e f)) @~a{(require (for-syntax a b) - (for-meta 2 c - d) + (for-meta 2 + c + d) e - f) - - }) - (check-equal? (require-pretty-format + f)}) + (check-equal? (format-require `(require (only-in m a b) (except-in m a b))) @~a{(require (only-in m a b) (except-in m a - b)) - - })) + b))})) -;; Pretty print a require form with one module per line and with -;; indentation for the `for-X` subforms. Example: +;; Rint a require form. +;; +;; All forms are indented with `require` indent style. +;; +;; Unlike pretty-print, we always print the head item plus the first +;; arg on the same line, then any subsequent items each on their own +;; line indented with the first arg. We don't use pretty-print's +;; approach of hot or cold porridge -- i.e. trying to squeeze forms +;; onto the same line <= pretty-print-columns, otherwise every item on +;; its own line all indented with the head. Instead we want warm +;; uniformity. (But we break that rule for module-path forms like +;; `submod`, which we print on one line.) ;; -;; (require (for-syntax racket/base -;; syntax/parse) -;; (for-meta 3 racket/a -;; racket/b) -;; racket/format -;; racket/string -;; "a.rkt" -;; "b.rkt") -(define/contract (require-pretty-print x) - (-> list? any) - (define (prn x first? indent) - (define (indent-string) - (if first? "" (make-string indent #\space))) - (define (prn-form pre this more) - (define new-indent (+ indent (+ 2 (string-length pre)))) - (printf "~a(~a " (indent-string) pre) - (prn this #t new-indent) - (for ([x more]) +;; Also there are details like printing `[old-id new-id]` forms with +;; square brackets, by convention. +;; +;; Note: Although it might be more robust to write this in a grammar +;; style, the following is less tedious and seems OK. +(define (print-require v) + (struct text (v)) ;a value to `display` not `write` + (let prn ([v v] [indent? #t] [indent 0]) + (define (maybe-indent) + (when indent? + (for ([_ (in-range indent)]) + (display #\space)))) + (define (prn-form head this more) + (define head-str (symbol->string head)) + (define new-indent (+ indent 2 (string-length head-str))) + (maybe-indent) + (printf "(~a " head-str) + (prn this #f new-indent) + (for ([v more]) (newline) - (prn x #f new-indent)) + (let ([v (match v + [(list old new) + #:when (memq head '(rename-in only-in)) + (text (format "[~a ~a]" old new))] + [v v])]) + (prn v #t new-indent))) (display ")")) - (match x - [(list 'require) - (void)] - [(list* (and pre (or 'require 'for-syntax 'for-template 'for-label - 'only-in 'except-in)) - this more) - (prn-form (format "~s" pre) this more) - (when (eq? pre 'require) - (newline))] - [(list* 'for-meta level this more) - (prn-form (format "for-meta ~a" level) this more)] - [this - (printf "~a~s" (indent-string) this)])) - (prn x #t 0)) + (match v + [(and (or (? string?) + (? symbol?) + (cons (or 'submod 'quote 'lib 'file 'planet '= '+ '-) _)) + mod-path) + (maybe-indent) + (write mod-path)] + [(list* (? symbol? head) this more) + (prn-form head this more)] + [(text s) + (maybe-indent) + (display s)] + [v ;Some other value, like number or #f for phase-level + (maybe-indent) + (write v)]))) + +(module+ test + (check-equal? (format-require + '(require + (prefix-in pre: + (combine-in + (only-in "a.rkt" b [c d]) + (rename-in (submod m sub) [y z] [p q]))))) + @~a{(require (prefix-in pre: + (combine-in (only-in "a.rkt" + b + [c d]) + (rename-in (submod m sub) + [y z] + [p q]))))})) diff --git a/test/example/requires.rkt b/test/example/requires.rkt new file mode 100644 index 00000000..9b1f6a6c --- /dev/null +++ b/test/example/requires.rkt @@ -0,0 +1,14 @@ +#lang racket + +(module m racket/base + (require net/uri-codec + net/url) + get-pure-port) + +(match 1 [v #f]) +first +rest + +(require (combine-in racket/format net/url)) +(require net/url) +~a