Skip to content

Releases

v0.10.0

Choose a tag to compare

@dpep dpep released this 03 Nov 07:55
7c5410f
  • Upgrading reindexes once (store v75): the first command after
    upgrading rebuilds the index, with nothing to run. A script reading
    --dead's summary.tiers now sees methods under test-only (below).

Added

  • Haml templates are read. Every *.haml in a checkout is indexed for
    the Ruby it runs, at the template's own lines and columns, as an ERB
    template is: a script line's code (- x, = x, != x, &= x, ~ x, a
    tag's = x, and the lines a trailing comma continues it onto), a tag's
    attribute hash and (name=value) values, each #{…} in text and in a
    filter, and a :ruby filter's body. A call in one under app/views/ runs
    on the view context (resolved_via: view), so --def and --refs
    answer there, and --dead counts it as a call rather than "named in a
    view (…), which is not read" — that caveat stays for Slim, Jbuilder and
    Builder. An if/elsif/else/unless chain is read flat, its
    conditions and branches each a statement. A do block's parameters are
    in scope inside it and not after it, except where two blocks end at the
    left margin at once, which leaves room to close only one: there the
    outer block's parameters stay in scope over the Ruby up to its close.
    The language server reads .haml documents and says so
    (experimental.trekr.templates lists "haml"), and the Claude Code
    plugin starts it for .haml files. The VS Code extension's Haml support
    ships separately (see its changelog).

  • --dead lists a method only specs and tests call as test-only, as
    it already did a class, however many calls they write. Such a method was
    missing when the specs called it more than once, and single-caller when
    once. The reason says how many calls and where the first is, and a method
    row's test_refs counts them, as a constant row's does. A test is a file
    under a spec/, test/ or tests/ directory at the checkout's root or
    a nested package's (plugins/chat/spec/), not one inside lib/ or
    app/. A method that overrides another, or a hook Ruby calls by name,
    keeps the tier it would have without the tests, as does one a route or a
    library's convention reaches (a Pundit predicate), and a spec's own
    helper is never test-only.

  • --dead says when a row is reached from outside the repo. Such a row
    keeps its tier, is graded lower, and names its kind of entry point
    (entry_point in JSON); its caveat names the evidence that settles it.

    • route: a routed controller action, or a controller only routes name
      (these were convention-only, clear). Settle it with request logs or
      APM.
    • job: an Active Job or Sidekiq job class nothing outside the tests
      names. Settle it with job metrics or logs.
    • api: a library's public API — a public or protected method (RDoc
      documents protected ones), a class or module, or a constant bound to a
      constant path, resolved or not (Worker = Job, Client = OtherGem::Client), or to a DeprecatedConstantProxy, in a library's
      lib/. Any other value constant is not API. Settle it with the gem's
      dependents or usage data.

    A library's lib/ is the checkout's own, or one beside a nested
    *.gemspec (protobuf's ruby/), in a checkout that is no app. An app has
    a config/application.rb at its root, gemspec or not, or a config.ru
    or an app/ without a root *.gemspec — so an engine gem's app/ or a
    gem's demo config.ru does not make one. A library needs no gemspec: a
    Hoe gem such as minitest has none until release. An app's own engine is
    not a library.

    What its author marks internal is not API: :nodoc:, @api private,
    @private (a YARD tag opening a comment line), Internal: (a TomDoc
    status opening the comment's first line), a leading underscore, or
    private_constant. Each mark reaches as far as its tool reads it:

    • A :nodoc: covers its own class and the methods its body defines, as
      RDoc reads it, so Rails' module ActiveSupport # :nodoc: hides nothing
      nested in it. In that body, an own-line # :doc: at a method's
      indentation uncovers the method, unless an own-line # :nodoc: at that
      indentation comes between (Rails' Dependencies.run_interlock).
    • :nodoc: all and @api private also cover what their marked opening
      holds, but not another opening of the class; private_constant covers
      what is nested anywhere.
    • A method's own @api public overrides its class's @api private, as
      YARD reads it.
    • A private or underscored method marked :doc: is API, as RDoc
      documents it (Rails' RequestForgeryProtection#verified_request?).
    • A method under a bare module_function is API by its public singleton
      copy, as one named by module_function :x is, until a bare private
      ends that mode.
  • An RSpec predicate matcher is a reference to the predicate it calls.
    expect(page).to have_event_count(1) calls has_event_count?, and
    be_visible and be_an_admin call visible? and admin?. be_exist
    calls exist? when the subject has it, else exists?, as RSpec's
    method_missing does. --refs lists each such call (called_as names
    the matcher), the editor's Find References lists it too, and --dead
    counts it, so a page object's predicate a spec asserts this way is no
    longer unreferenced. Confirmed where expect(x) types its subject,
    possible where it does not (be_exist then for both exist? and
    exists?); a matcher RSpec or the spec defines (be_within) calls no
    predicate.

  • --dead lists a Minitest or Test::Unit test as convention-only,
    by the test runner, rather than unreferenced: a public test_ method
    of a class under a test directory that inherits Minitest::Test or its
    kin, or, where its ancestors are not all indexed, is in a test file
    (test_*.rb, *_test.rb) under test/. A class in a test file that is
    no test case (a fake, a stub controller) runs no test_ method.

  • --dead reads an extensionless Ruby script (exe/tool, bin/x),
    which trekr does not index, for the names it writes: a row one names
    keeps its tier and is lower, "named in a script (exe/tool), which is
    not read", as a Slim view's word grades it.

  • --dead reads an interpolated string sent by name,
    TopicQuery.new(opts).public_send("list_#{filter}") or rule = "validate_#{name}"; public_send(rule), as it reads :"list_#{filter}".
    A method whose name fits, in the receiver's class, its ancestors, its
    subclasses or theirs, says "a name of its shape is sent at runtime" and
    is graded lower. A string sent to a variable
    (row.public_send("total_#{kind}")) is left alone.

Changed

  • --dead grades a class, module or constant row clear when nothing in
    the checkout could reach it by a built name
    , instead of lower on
    every row. A row is lower when the checkout looks up a name computed
    whole (name.constantize, Object.const_get(x), klass.const_get(x)),
    and its caveat says where, or when a namespace, shape or listing already
    caveated reaches it. Apps nearly always have such a lookup, so their rows
    stay lower; a gem without one now reads clear.
  • --dead grades a module's method lower when its name is called on a
    class that lacks it
    , naming the call: "called on TopicQuery (…), where
    trekr finds no such method; a module mixed in at runtime may supply it".
    A module prepended from a block (a plugin's after_initialize) is no
    edge trekr records, so such a method was unreferenced, clear, though
    that call runs it.

Fixed

  • A bare private, protected or public ends module_function's
    mode
    , as in Ruby: a method after one is no longer recorded as a public
    singleton copy plus a private instance method, so --symbols and
    --def give it the visibility its toggle says, and no singleton method
    Ruby never defines. private def x, public def x and protected def x are no toggle: they keep the mode's public singleton copy, and the
    instance method takes the visibility written.
  • A def in Target.class_eval do … end inside another class or method
    is Target's
    , as it already was at the top level. It was recorded on
    the class around the block, so calls to it were residue and --refs 'Target#x' said no_such_method. Target.instance_eval do def x makes
    Target.x. The def's owner is found as Ruby finds Target, through
    the enclosing class's ancestors too, and an alias (Gizmo = Gadget) is
    followed. A constant written in a qualified receiver's block
    (Faker::Lorem.instance_eval { X = 1 }) is scoped like Ruby's: top-level
    there, not the receiver's.
  • --dead no longer lists a class or module that a spec or test file
    defines and the tests use as test-only: it is test code in use, as a
    spec's own helper method is. One nothing uses is still unreferenced.
  • --dead no longer takes a test/ or spec/ directory inside lib/ or
    app/ for tests: rack-test's library is lib/rack/test/, so a constant
    only it used was test-only, and a name it builds and sends was not read.
  • --dead no longer says a method "calls super, so it overrides a method
    trekr has not indexed", and grades it lower, when the super belongs
    to a def nested in its body or to a block it hands to define_method
    or define_singleton_method (send(:define_method, :x) { super() }).
  • --dead counts a test's reference to a class once. A class written with
    a compact namespace nothing else defines (class Scheduler::X) had each
    reference counted twice in test_refs and its "referenced only from
    tests (N, …)" reason.

v0.9.8

Choose a tag to compare

@dpep dpep released this 18 Oct 15:57
ec15fce
  • A large branch switch holds the write lock for less time again: recording
    the files the switch changed took 0.2–0.4 s on rails, and now takes tens of
    milliseconds, so editor saves and other indexes wait less behind it.

v0.9.7

Choose a tag to compare

@dpep dpep released this 18 Oct 15:34
6b77a9a
  • A large branch switch holds the write lock about as briefly as 0.9.5 did.
    0.9.6 spent up to twice as long recording which versions it had replaced,
    and editor saves and other indexes waited behind it.

v0.9.6

Choose a tag to compare

@dpep dpep released this 18 Oct 06:03
baaf667
  • Replaced versions of files are no longer kept forever. A version an
    edit, a branch switch or --drop left behind was kept so a switch back
    would not reparse — and never collected, so a store in a repo edited
    between indexes grew by every edit (rails, 100 files edited a day: +1.1
    MB a day, without end). It is now kept for 14 days after nothing maps
    it, then collected by the background cleanup or --gc; on rails the
    same edits level off at day 14. A switch back within the retention still
    reuses everything; after it, the files are read again (2,611 rails files
    in about 2 s). An existing index loses nothing on upgrade: what it
    already holds is counted from then.
  • TREKR_RETENTION sets how long — 36h, 14d, 2w, 0, or never
    to keep every version as before — and --retention overrides it for one
    --gc or --status. Set it where every trekr sees it: the background
    cleanup reads the environment, not the flag.
  • --gc --older-than now judges checkouts only. A query's copy of an
    edit went at --older-than; it now goes by the retention, as every
    unmapped version does. To collect them all now, trekr --gc --retention 0.
  • --status reports what is kept (storage.retained: blobs, expired,
    retention); --gc reports retention and, among its blobs, the
    expired ones.
  • An index no longer fails when a collection runs beside it. An index
    that found a file's version known, then waited for the write lock while
    --gc or the background cleanup deleted that version, failed "Query
    returned no rows" (exit 74). It now decides what is known once it holds
    the lock, and reads such a file again. An editor's save of a version a
    collection had just deleted kept the file's previous version until the
    next save; it is now parsed.
  • --older-than refuses an age too long to count (exit 64), as
    TREKR_RETENTION and --retention do; it wrapped round to a short one.

v0.9.5

Choose a tag to compare

@dpep dpep released this 18 Oct 01:53
1310743
  • The index cleans up after itself. An upgrade's rebuild left its old
    rows' space free inside the file (a 229 MB store stayed 229 MB holding
    75 MB), and each release left a stale tree snapshot for every checkout
    not queried since; only trekr --gc --vacuum, which nothing mentioned,
    gave them back. Now, after an index pass — the one a query or the editor
    starts, or trekr --index — trekr measures the waste at most every two
    hours, and when it is at least 32 MB and a quarter of the file, a
    background process removes the stale snapshots, collects the blobs only
    a query read a week ago, and gives the free space back to the disk a few
    megabytes at a time, so an index or edit waits a fraction of a second,
    not for the whole compaction. A store from before this release is
    rewritten once instead (through a temporary file, when the disk has room
    for twice its live size), which switches it. It never runs on a query,
    waits for nothing, and skips a store another trekr is writing. Nothing
    to do: a store 0.9.4 rebuilt shrinks after its next index.
  • --gc collecting many checkouts is ~35× faster. Each blob it
    deleted scanned a table of class-body calls: removing 240 checkouts from
    a 1 GB index took 34 minutes, and takes 56 s now. An existing index gains
    the missing index in the background, or at the start of the next --gc
    — a dry run included, which says so (indexes) and removes nothing;
    nothing is rebuilt.
  • --status says how big the index is and what it could give back
    (storage in --json), when the background cleanup last ran, and why
    it left space it meant to give back (no_room: the disk lacks room to
    rewrite the index once).
  • --gc's reclaimed_bytes is what left the disk — the snapshots,
    copies and stores it removed, and the file's shrink after --vacuum. It
    said 0 after removing snapshots, since it counted only the pages freed
    inside the file; those are freed_bytes now, and free_bytes is what is
    left free.
  • A question that indexes its checkout first no longer sometimes builds
    the tree twice.
    When the index it started finished its rows before the
    query's first look, the query stopped waiting for it and assembled the
    tree itself while the index prepared the same one.
  • Hover at a constant's definition in class Object names it OBJC,
    as hover at a mention already did; it said Object::OBJC.

v0.9.4

Choose a tag to compare

@dpep dpep released this 17 Oct 16:03
8bd222d
  • A constant written in class Object is the top level's. Ruby reaches
    class Object; OBJC = 6; end as OBJC, ::OBJC and Object::OBJC
    alike, and a top-level TOP = 1 as Object::TOP; trekr kept them apart,
    so ::OBJC, Object::TOP, and a bare OBJC at the top level or in a
    module's method were residue, and --refs by each spelling listed a
    different set. They now resolve, hover and go to the definition, and
    --refs ::OBJC, --refs Object::OBJC and Find References at any of them
    list the same mentions. Hover at a mention names such a constant OBJC,
    as Ruby does.
    A module's lookup, and ::KC, also reach a Kernel constant now, as in
    Ruby; Box::OBJC and Box::TOP still miss.
  • --dead counts a top-level constant used as Object::X or ::X.
    class Object; LIMIT = 1; end read only as ::LIMIT, or TOP = 1 read
    only as Object::TOP, was listed unreferenced.
  • --refs by a constant's whole name lists mentions made through an
    ancestor.
    --refs Kernel::KC missed Box::KC and Leaf::KC, which
    Ruby resolves through Box's and Leaf's ancestors, and so did --refs at
    its definition, while Find References in an open file listed them. Every
    mention that resolves to the constant is now listed, by name, at the
    definition and at any mention, in the CLI and the editor alike. The store
    is rebuilt on first use (v66): the next command reindexes.
    The rebuild leaves the old rows' space free inside the file; run
    trekr --gc --vacuum once afterwards to shrink it.

v0.9.3

Choose a tag to compare

@dpep dpep released this 10 Oct 22:08
2c88c85
  • Box::OBJC no longer finds a constant written in class Object.
    Ruby (since 2.5) raises NameError when a scoped lookup reaches a constant
    only through Object, but --def, hover, Go to Definition and Find
    References resolved it to Object::OBJC, and the --refs Box::OBJC
    miss said it was found through Box's ancestor Object. Object::OBJC, a
    bare OBJC inside a class, and a Kernel constant reached as Box::KC
    still resolve, as in Ruby.

v0.9.2

Choose a tag to compare

@dpep dpep released this 10 Oct 16:18
846482b
  • A new file opened while git is slow for another lands with its open.
    In the editor, a file git placed at once was written only at the next
    retry, a quarter second later, whenever another file's git ls-files was
    still out, so the request right after its open missed it. Opens behind a
    pending answer now share a 100 ms wait.
  • A path gem's copy of a stdlib file answers alone. require "json"
    where a Bundler path: gem has its own lib/json.rb answered that copy
    and the stdlib's, ambiguous; Bundler puts a path gem's lib/ ahead of
    the stdlib as surely as an installed gem's, so it is now resolved. A copy
    in the checkout's own lib/, test/ or spec/ is still listed beside
    the stdlib's, since those are only guessed to be on the load path.
  • A --refs miss on a name reached through an ancestor says where it
    is.
    --refs File::NULL read as if nothing defined it; the mentions are
    listed under IO::NULL, where Ruby finds it through File < IO. The
    miss now adds "found through File's ancestor IO, it is IO::NULL" (or,
    through an alias, the name Ruby's lookup reaches). Which rows are found
    is unchanged.

v0.9.1

Choose a tag to compare

@dpep dpep released this 10 Oct 06:41
d00dfb1
  • A constant assigned in class << self is the singleton class's.
    --refs, --def, --dead, hover and Go to Definition took it for the
    class's own, so Widget::LIMIT and an instance method's LIMIT were
    listed as its references, though Ruby raises NameError on both. It is now
    found only from inside that body, by position or by Ruby's own spelling
    of its whole name, --refs Widget::singleton_class::LIMIT
    (--refs Widget::LIMIT misses, and says the name it has); --dead and
    workspace symbol search name its owner Widget::singleton_class. A class
    opened in that body is named the same way, and so are its methods:
    --refs 'Widget::singleton_class::Job#perform'. A class << self method
    no longer resolves a constant of the class's superclass or includes,
    which Ruby does not find there either.

  • This changes what an index records (store v65): trekr reindexes once after
    upgrading.

  • A file opened behind a slow git is indexed once git answers. In
    the editor, a new file whose git ls-files took over 1 s was left out
    of the index for the session, and each open or save held every request
    for that second. git is now asked off the request loop and waited for at
    most 400 ms, once for files opened together, and the file lands when it
    answers; a git that has not answered in 10 s is stopped and
    the next save asks again. An ignored file is still never written.

v0.9.0

Choose a tag to compare

@dpep dpep released this 09 Oct 18:45
bfd93e0
  • index.cause says when another trekr was writing. An answer whose
    edited files could not be read because another trekr held the store
    said stale: true with no cause; cause now names those files and
    why.

  • A call inside a made name's #{…} is that call. In
    class_eval("def #{dir.upcase}_shout"), every letter of upcase
    answered the method the string makes (UP_shout), with full
    confidence. A call or constant written inside such a name now answers
    itself, as a variable there already did.

  • Go to Definition in a file the index has not read uses its own
    class.
    In an untracked file created after the last index (with no
    file watcher to say so), a call inherited from the file's superclass
    listed every method of that name; --def answered the superclass's.
    Opening a file now reads it as saving it does — one the index would
    read, that is: opening or saving a file git ignores leaves the index
    as it was.

  • --gc collects what queries recorded. Each edited version of a file
    a query read stayed in the store for good; --gc now removes those no
    checkout maps once they are older than --older-than (and than an hour,
    so a query still reading one keeps it), and exits 0 when that is all it
    removed. Blobs an --index read or mapped are kept as before.

  • A spec macro's or store_accessor's method ends where its argument
    does.
    The column just past let(:name), subject(:name) or
    RSpec::Matchers.define :name answered that method, and
    store_accessor :settings, :"key" placed each accessor with its own
    name's length from the : — short of the closing quote for key, far
    past it for key_changed?. Each now spans its symbol (or string) as
    written, closing quote included. An association's generated methods
    (dogs=, dog_ids from has_many :dogs) still reach past it.

  • --refs by a constant's whole name lists the constant as its scopes
    write it.
    --refs Admin::Widget listed only mentions written
    Admin::Widget, missing Widget inside module Admin; on
    SEPARATORS.join inside module Router it said "no mention of
    Router::SEPARATORS"; and on a constant's definition (SIZES = …) it was a
    usage error, exit 64. A name written with :: now lists the definitions
    and the references Ruby's lookup resolves to it through the scopes they
    are written in, as the editor's Find References does, and a position on
    any class, module or constant answers that way. A constant nothing
    defines whole is named by the part that resolves: Nodes::Union inside
    module Arel is Arel::Nodes::Union. By name, a path through an
    includer, a subclass or an alias (Child::K) is not found; at the
    mention it is. A top-level constant's whole name is ::Widget; a bare
    --refs Widget still lists every mention of the name.

  • A --refs miss is said as a no. "no mention of X (indexed? try
    trekr --index)" sent an agent to reindex a checkout already indexed;
    it now reads "no mention of X in what trekr indexed", and for a name
    written with :: says it was read as a whole name from the top level.

  • --def in a require string answers the file it loads. Anywhere in
    require_relative "../../lib/helper", --def snapped to
    Kernel#require_relative, while Go to Definition opened lib/helper.rb.
    Both now ask one subject: the file, at its top, with under: require and
    resolved_via: require in --json, and a reason when no file is found.
    A gem's copy of a stdlib file (require "json", "erb", "uri") is
    the one answered, as Ruby loads it; Go to Definition offered both. In a
    monorepo, a file inside a path gem also searches that gem's own test/
    and spec/: require "cases/helper" in rails' activerecord/test
    opens activerecord/test/cases/helper.rb.

  • --def on a method's def names its Sorbet signature. Go to
    Declaration from a def opened the .rbi stub that describes it, while
    --def there answered only the def itself. Its --json now carries
    signatures, as an answer on a call of the method does.