diff --git a/default-recommendations/analyzers/binding-origin-test.rkt b/default-recommendations/analyzers/binding-origin-test.rkt new file mode 100644 index 0000000..78b7ccc --- /dev/null +++ b/default-recommendations/analyzers/binding-origin-test.rkt @@ -0,0 +1,44 @@ +#lang resyntax/test + + +require: resyntax/default-recommendations default-recommendations +header: - #lang racket/base + + +analysis-test: "local variable use marked as originating from a lexical binding" +-------------------- +(let ([x 1]) + (void x)) +-------------------- +@within - (void x) +@inspect - x +@property binding-origin +@assert lexical + + +analysis-test: "#lang racket/base function use marked as originating from required module" +- (void) +@inspect - void +@property binding-origin +@assert required-module + + +analysis-test: "imported function use marked as originating from required module" +-------------------- +(require racket/list) +(first '(a b c)) +-------------------- +@inspect - first +@property binding-origin +@assert required-module + + +analysis-test: "module variable use marked as originating from surrounding file" +-------------------- +(define x 1) +(void x) +-------------------- +@within - (void x) +@inspect - x +@property binding-origin +@assert local-module diff --git a/default-recommendations/analyzers/binding-origin.rkt b/default-recommendations/analyzers/binding-origin.rkt new file mode 100644 index 0000000..937ffc7 --- /dev/null +++ b/default-recommendations/analyzers/binding-origin.rkt @@ -0,0 +1,51 @@ +#lang racket/base + + +(require racket/contract/base) + + +(provide + (contract-out + [binding-origin-analyzer expansion-analyzer?])) + + +(require racket/match + racket/treelist + rebellion/streaming/reducer + resyntax/default-recommendations/analyzers/private/expansion-identifier + resyntax/private/analyzer + resyntax/private/logger + resyntax/private/syntax-property-bundle) + + +;@---------------------------------------------------------------------------------------------------- + + +(define (binding-origin stx) + (for/reducer into-syntax-property-bundle + ([id (in-treelist (expanded-syntax-identifiers stx))] + #:when (equal? (expansion-identifier-kind id) 'usage) + #:do [(define origin (expansion-identifier-binding-origin id))] + #:when origin) + (syntax-property-entry (expansion-identifier-path id) 'binding-origin origin))) + + +(define (expansion-identifier-binding-origin exp-id) + (define id-stx (expansion-identifier-syntax exp-id)) + (define phase (expansion-identifier-phase exp-id)) + (define mod (expansion-identifier-enclosing-module exp-id)) + (define binding (identifier-binding id-stx phase #false #false)) + (match binding + ['lexical 'lexical] + [(list defining-mod _ ...) + (define-values (path base) (module-path-index-split defining-mod)) + (cond + [(and (not base) (not path)) 'local-module] + [else 'required-module])] + [_ + (log-resyntax-debug "binding for ~a: ~a" id-stx binding) + #false])) + + +(define binding-origin-analyzer + (make-expansion-analyzer binding-origin #:name 'binding-origin-analyzer)) diff --git a/default-recommendations/analyzers/identifier-usage-test.rkt b/default-recommendations/analyzers/identifier-usage-test.rkt index 1e787ee..bf98373 100644 --- a/default-recommendations/analyzers/identifier-usage-test.rkt +++ b/default-recommendations/analyzers/identifier-usage-test.rkt @@ -337,3 +337,43 @@ analysis-test: "twice-used internal function definition in function" @inspect - g @property usage-count @assert 2 + + +analysis-test: "unused local variable in macro definition" +-------------------- +(require (for-syntax racket/base)) +(define-syntax (m stx) + (define a 1) + stx) +-------------------- +@inspect - a +@property usage-count +@assert 0 + + +analysis-test: "once-used local variable in macro definition" +-------------------- +(require (for-syntax racket/base)) +(define-syntax (m stx) + (define a 1) + (void a) + stx) +-------------------- +@within - (define a 1) +@inspect - a +@property usage-count +@assert 1 + + +analysis-test: "twice-used local variable in macro definition" +-------------------- +(require (for-syntax racket/base)) +(define-syntax (m stx) + (define a 1) + (void a a) + stx) +-------------------- +@within - (define a 1) +@inspect - a +@property usage-count +@assert 2 diff --git a/default-recommendations/analyzers/private/expansion-identifier.rkt b/default-recommendations/analyzers/private/expansion-identifier.rkt new file mode 100644 index 0000000..886d117 --- /dev/null +++ b/default-recommendations/analyzers/private/expansion-identifier.rkt @@ -0,0 +1,97 @@ +#lang racket/base + + +(require racket/contract/base) + + +(provide + (struct-out expansion-identifier) + (contract-out + [expanded-syntax-identifiers (-> syntax? (treelist/c expansion-identifier?))])) + + +(require racket/list + racket/stream + racket/treelist + resyntax/private/syntax-path + resyntax/private/syntax-traversal + syntax/parse) + + +;@---------------------------------------------------------------------------------------------------- + + +(struct expansion-identifier (syntax path phase enclosing-module kind) + #:transparent + #:guard (struct-guard/c identifier? + syntax-path? + exact-nonnegative-integer? + module-path-index? + (or/c 'binding 'usage))) + + +(define (in-expanded-syntax-identifiers orig-expanded-stx) + (define labeled-stx (syntax-label-paths orig-expanded-stx 'expanded-path)) + (let loop ([expanded-stx labeled-stx] [phase 0] [skip? #false] [parents '()]) + + (define (recur stx #:phase [phase phase] #:skip-root? [skip? #false] #:parents [parents parents]) + (loop stx phase skip? parents)) + + (define (make-expanded-identifier id-stx kind) + (define path (syntax-property id-stx 'expanded-path)) + (define mod (module-path-index-join #false #false (and (not (empty? parents)) parents))) + (expansion-identifier (syntax-ref orig-expanded-stx path) path phase mod kind)) + + (syntax-search expanded-stx + #:skip-root? skip? + #:literal-sets ([kernel-literals #:phase phase]) + + [id:id (stream (make-expanded-identifier (attribute id) 'usage))] + + [(begin-for-syntax _ ...) (recur this-syntax #:phase (add1 phase) #:skip-root? #true)] + + [((~or module-id:module module-id:module*) name _ body) + (define name-sym (syntax->datum (attribute name))) + (stream-append (recur (attribute module-id)) + (recur (attribute body) #:phase 0 #:parents (append parents (list name-sym))))] + + [(quote-syntax _ ...) (stream)] + + [(define-values-id:define-values (id ...) body) + (define exp-ids + (cons (make-expanded-identifier (attribute define-values-id) 'usage) + (for/list ([id-stx (in-list (attribute id))]) + (make-expanded-identifier id-stx 'binding)))) + (stream-append exp-ids (recur (attribute body)))] + + [(define-syntaxes-id:define-syntaxes (id ...) body) + (define exp-ids + (cons (make-expanded-identifier (attribute define-syntaxes-id) 'usage) + (for/list ([id-stx (in-list (attribute id))]) + (make-expanded-identifier id-stx 'binding)))) + (stream-append exp-ids (recur (attribute body) #:phase (add1 phase)))] + + [((~or let-id:let-values let-id:letrec-values) ([(id ...) rhs] ...) body ...) + (define exp-let-id (make-expanded-identifier (attribute let-id) 'usage)) + (define exp-ids + (for*/list ([id-list (in-list (attribute id))] + [id-stx (in-list id-list)]) + (make-expanded-identifier id-stx 'binding))) + (define inner-exprs (append (attribute rhs) (attribute body))) + (apply stream-append (stream exp-let-id) exp-ids (map recur inner-exprs))] + + [(#%plain-lambda formals body ...) + (apply stream-append + (syntax-search (attribute formals) + [id:id (stream (make-expanded-identifier (attribute id) 'binding))]) + (map recur (attribute body)))] + + [(case-lambda [formals body ...] ...) + (apply stream-append + (syntax-search #'(formals ...) + [id:id (stream (make-expanded-identifier (attribute id) 'binding))]) + (map recur (append* (attribute body))))]))) + + +(define (expanded-syntax-identifiers orig-expanded-stx) + (sequence->treelist (in-expanded-syntax-identifiers orig-expanded-stx))) diff --git a/default-recommendations/analyzers/private/expansion-module.rkt b/default-recommendations/analyzers/private/expansion-module.rkt new file mode 100644 index 0000000..ad76776 --- /dev/null +++ b/default-recommendations/analyzers/private/expansion-module.rkt @@ -0,0 +1,50 @@ +#lang racket/base + + +(require racket/contract/base) + + +(provide + (struct-out expansion-module) + (contract-out + [expanded-syntax-modules (-> syntax? (hash/c (treelist/c symbol?) expansion-module?))])) + + +(require racket/stream + racket/treelist + resyntax/private/syntax-path + resyntax/private/syntax-traversal + syntax/parse) + + +;@---------------------------------------------------------------------------------------------------- + + +(struct expansion-module (syntax path name declaration-phase parents kind prelude) + #:transparent + #:guard (struct-guard/c syntax? + syntax-path? + symbol? + exact-nonnegative-integer? + (treelist/c symbol?) + (or/c 'module 'module*) + (or/c module-path? #false))) + + +(define (expanded-syntax-modules orig-expanded-stx) + (define results + (let loop ([stx (syntax-label-paths orig-expanded-stx 'expanded-path)] + [phase 0] + [parents (treelist)]) + (syntax-search stx + #:literal-sets ([kernel-literals #:phase phase]) + [(module name prelude body) + (define path (syntax-property this-syntax 'expanded-path)) + (define name-sym (syntax->datum (attribute name))) + (define prelude-datum (syntax->datum (attribute prelude))) + (define exp-mod + (expansion-module this-syntax path name-sym phase parents 'module prelude-datum)) + (stream-cons exp-mod (loop (attribute body) 0 (treelist-add parents name-sym)))]))) + (for/hash ([expmod (in-stream results)]) + (define ancestry (treelist-add (expansion-module-parents expmod) (expansion-module-name expmod))) + (values ancestry expmod))) diff --git a/default-recommendations/analyzers/private/id-util.rkt b/default-recommendations/analyzers/private/id-util.rkt new file mode 100644 index 0000000..fb81712 --- /dev/null +++ b/default-recommendations/analyzers/private/id-util.rkt @@ -0,0 +1,124 @@ +#lang racket + +(require racket/phase+space + rebellion/type/record + rebellion/type/singleton + resyntax/private/syntax-traversal + syntax/parse + (only-in racket/base [identifier-binding racket:identifier-binding])) + + +(define-singleton-type lexical-binding) + + +(define-record-type local-module-binding + (name submodule phase)) + + +(define-record-type imported-module-binding + (export-site-module + export-site-name + export-site-phase + export-site-space + import-site-phase + import-site-space)) + + +(define-record-type proxied-module-binding + (origin-site-module + origin-site-name + origin-site-phase + proxy-site-module + proxy-site-name + proxy-site-phase + proxy-site-space + import-site-phase + import-site-space)) + + +(struct top-level-binding (name) #:transparent) + + +(define-singleton-type unbound) + + +(define (identifier-binding id-stx + #:phase [phase-level (syntax-local-phase-level)] + #:exact-scopes? [exact-scopes? #false]) + (match (racket:identifier-binding id-stx phase-level #true exact-scopes?) + ['lexical lexical-binding] + [(list top-name) (top-level-binding top-name)] + [#false unbound] + [(list from-mod + from-sym + nominal-from-mod + nominal-from-sym + from-phase + import-phase+space-shift + nominal-export-phase+space) + (cond + [(self-module-path-index? from-mod) + (local-module-binding + #:name from-sym + #:submodule (module-path-index-submodule from-mod) + #:phase from-phase)] + [(equal? from-mod nominal-from-mod) + (define from-space (phase+space-space nominal-export-phase+space)) + (define-values (phase-shift import-space) + (match import-phase+space-shift + [(cons phase-shift new-space) (values phase-shift new-space)] + [phase-shift (values phase-shift from-space)])) + (imported-module-binding + #:export-site-name from-sym + #:export-site-module from-mod + #:export-site-phase from-phase + #:export-site-space from-space + #:import-site-phase (+ from-phase phase-shift) + #:import-site-space import-space)] + [else + (define nominal-export-phase (phase+space-phase nominal-export-phase+space)) + (define nominal-export-space (phase+space-space nominal-export-phase+space)) + (define-values (phase-shift import-space) + (match import-phase+space-shift + [(cons phase-shift new-space) (values phase-shift new-space)] + [phase-shift (values phase-shift nominal-export-space)])) + (proxied-module-binding + #:origin-site-name from-sym + #:origin-site-module from-mod + #:origin-site-phase from-phase + #:proxy-site-name nominal-from-sym + #:proxy-site-module nominal-from-mod + #:proxy-site-phase nominal-export-phase + #:proxy-site-space nominal-export-space + #:import-site-phase (+ nominal-export-phase phase-shift) + #:import-site-space import-space)])])) + + +(define (identifier-binding-map id-stx #:exact-scopes? [exact-scopes? #false]) + (for/hash + ([phase (in-list (syntax-bound-phases id-stx))] + #:do [(define binding (identifier-binding id-stx #:phase phase #:exact-scopes? exact-scopes?))] + #:unless (or (top-level-binding? binding) (unbound? binding))) + (values phase binding))) + + +(define (self-module-path-index? mpi) + (define-values (path base) (module-path-index-split mpi)) + (and (not path) (not base))) + + +(define expstx + (expand + #'(module foo racket + (module m1 racket + (define x 1) + (provide x)) + (module m2 racket + (require (submod ".." m1)) + (provide x)) + (module m3 racket + (require (submod ".." m2)) + (void x))))) + +(for ([x (in-stream (syntax-search expstx [(~datum x)]))]) + (pretty-print (identifier-binding x))) diff --git a/default-recommendations/analyzers/variable-mutability.rkt b/default-recommendations/analyzers/variable-mutability.rkt index ff7b176..13a8137 100644 --- a/default-recommendations/analyzers/variable-mutability.rkt +++ b/default-recommendations/analyzers/variable-mutability.rkt @@ -13,7 +13,10 @@ racket/list racket/match racket/stream + racket/treelist + rebellion/streaming/reducer rebellion/streaming/transducer + resyntax/default-recommendations/analyzers/private/expansion-identifier resyntax/private/analyzer resyntax/private/syntax-path resyntax/private/syntax-property-bundle @@ -30,97 +33,33 @@ ;@---------------------------------------------------------------------------------------------------- -(define (syntax-label-id-phases expanded-stx) - (let loop ([expanded-stx expanded-stx] [phase 0] [skip? #false]) - (syntax-traverse expanded-stx - #:skip-root? skip? - #:literal-sets ([kernel-literals #:phase phase]) - - [:id (syntax-property this-syntax 'phase phase)] - [(begin-for-syntax _ ...) (loop this-syntax (add1 phase) #true)] - - [(define-syntaxes-id:define-syntaxes ids expr) - (define new-define-syntaxes (loop (attribute define-syntaxes-id) phase #false)) - (define new-ids (loop (attribute ids) phase #true)) - (define new-expr (loop (attribute expr) (add1 phase) #false)) - (define new-datum (list new-define-syntaxes new-ids new-expr)) - (datum->syntax this-syntax new-datum this-syntax this-syntax)] - - [((~or module module*) _ ...) (loop this-syntax 0 #true)] - - #:parent-context-modifier (λ (stx) stx) - #:parent-srcloc-modifier (λ (stx) stx) - #:parent-props-modifier (λ (stx) stx)))) - - -(define (binding-site-variables expanded-stx) - (let loop ([expanded-stx expanded-stx] [phase 0]) - (define (recur stx) - (loop stx phase)) - (syntax-search expanded-stx - #:literal-sets ([kernel-literals #:phase phase]) - - [(id:id _ ...) - #:do [(define id-phase (syntax-property (attribute id) 'phase))] - #:when (not (equal? id-phase phase)) - (loop this-syntax id-phase)] - - [(quote-syntax _ ...) (stream)] - - [(define-values (id ...) body) - (stream-append (attribute id) (recur (attribute body)))] - - [(define-syntaxes (id ...) body) - (stream-append (attribute id) (loop (attribute body) (add1 phase)))] - - [((~or let-values letrec-values) ([(id ...) rhs] ...) body ...) - (define inner-exprs (append (attribute rhs) (attribute body))) - (define ids (append* (attribute id))) - (apply stream-append ids (map recur inner-exprs))] - - [(#%plain-lambda formals body ...) - (apply stream-append - (syntax-search (attribute formals) [:id]) - (map recur (attribute body)))] - - [(case-lambda [formals body ...] ...) - (apply stream-append - (syntax-search #'(formals ...) [:id]) - (map recur (append* (attribute body))))]))) - - -(define (mutated-variables expanded-stx) - (let loop ([expanded-stx expanded-stx] [phase 0]) - (syntax-search expanded-stx - #:literal-sets ([kernel-literals #:phase phase]) - [(id:id _ ...) - #:do [(define id-phase (syntax-property (attribute id) 'phase))] - #:when (not (equal? id-phase phase)) - (loop this-syntax id-phase)] - [(quote-syntax _ ...) (stream)] - [(set! id:id expr) - (stream-cons (attribute id) (mutated-variables (attribute expr)))]))) - - (define (variable-mutability stx) - (define labeled-stx (syntax-label-id-phases (syntax-label-paths stx 'expanded-path))) + (define ids (expanded-syntax-identifiers stx)) (define variable-table (make-hash)) - (for ([id (in-stream (binding-site-variables labeled-stx))]) - (define phase (syntax-property id 'phase)) + (for ([id (in-treelist ids)] + #:when (equal? (expansion-identifier-kind id) 'binding)) + (define phase (expansion-identifier-phase id)) (define phase-specific-table (hash-ref! variable-table phase (λ () (make-free-id-table #:phase phase)))) - (free-id-table-set! phase-specific-table id 'immutable)) - (for ([id (in-stream (mutated-variables labeled-stx))]) - (define phase-specific-table (hash-ref variable-table (syntax-property id 'phase))) - (free-id-table-set! phase-specific-table id 'mutable)) - (transduce (in-hash-values variable-table) - (append-mapping in-dict-pairs) - (mapping - (λ (e) - (match-define (cons id mode) e) - (define path (syntax-property id 'expanded-path)) - (syntax-property-entry path 'variable-mutability mode))) - #:into into-syntax-property-bundle)) + (free-id-table-set! phase-specific-table (expansion-identifier-syntax id) 'immutable)) + (for ([id (in-treelist ids)] + #:do [(match-define (expansion-identifier id-stx path phase mod-names kind) id)] + #:when (and (equal? kind 'usage) + (not (equal? path empty-syntax-path)) + (equal? (treelist-last (syntax-path-elements path)) 1))) + (syntax-parse (syntax-ref stx (syntax-path-parent path)) + #:literals (set!) + [(set! _ _) + (define phase-specific-table + (hash-ref! variable-table phase (λ () (make-free-id-table #:phase phase)))) + (free-id-table-set! phase-specific-table id-stx 'mutable)] + [_ (void)])) + (for/reducer into-syntax-property-bundle + ([id (in-treelist ids)] + #:when (equal? (expansion-identifier-kind id) 'binding)) + (match-define (expansion-identifier id-stx path phase _ _) id) + (define mode (free-id-table-ref (hash-ref variable-table phase) id-stx)) + (syntax-property-entry path 'variable-mutability mode))) (define variable-mutability-analyzer diff --git a/private/source.rkt b/private/source.rkt index c7ff016..d0374f2 100644 --- a/private/source.rkt +++ b/private/source.rkt @@ -55,6 +55,7 @@ rebellion/collection/vector/builder rebellion/streaming/transducer rebellion/type/record + resyntax/default-recommendations/analyzers/binding-origin resyntax/default-recommendations/analyzers/identifier-usage resyntax/default-recommendations/analyzers/ignored-result-values resyntax/default-recommendations/analyzers/variable-mutability @@ -256,6 +257,8 @@ (define expansion-analyzer-props (transduce (sequence-append + (syntax-property-bundle-entries + (expansion-analyze binding-origin-analyzer expanded)) (syntax-property-bundle-entries (expansion-analyze identifier-usage-analyzer expanded)) (syntax-property-bundle-entries