dsherret / dsherret/ts-morph

finding references in local scope

Open
#1,351 0 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
6.2k
Forks
238
Avg merge
2m
Merged PRs (30d)
1

Description

Is your feature request related to a problem? Please describe.

i want to transform *only* the second x in this code

const x = 1
console.log(x)
{
  // scope start
  const x = 2 // declaration
  console.log(x) // reference
  {
    const x = 3
    console.log(x) // not a reference
  }
  // scope end
}
{
  const x = 4
  console.log(x)
}

like

-  const x = 2
+  const y = 2
-  console.log(x)
+  console.log(y)

but with Identifier#findReferencesAsNodes i get references in *all* scopes

docs: https://ts-morph.com/navigation/finding-references

Describe the solution you'd like

this should be a core feature, for example
Identifier#findLocalReferencesAsNodes or
Identifier#findReferencesInScopeAsNodes or

eslint

eslint-utils has

  // scope start
  const x = 2 // declaration
  console.log(x) // reference

example: eslint-plugin-react2solid/lib/rules/react-use-state-to-solid-create-signal.js

"use strict";

const eslintUtils = require("eslint-utils")

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "react useState to solid createSignal",
      recommended: true,
    },
    fixable: "code",
    schema: [],
  },
  create: function(context) {
    return {
      CallExpression: function(node) {
        if (
          (
            node.callee.type == "MemberExpression" &&
            node.callee.object.name == "React" &&
            node.callee.property.name == "useState"
          ) ||
          (
            node.callee.type == "Identifier" &&
            node.callee.name == "useState"
          )
        ) {
          let programNode = node
          while (programNode && programNode.type != "Program") {
            programNode = programNode.parent
          }
          // note: we must call context.report only once, to avoid double-fixing
          // example: someValue -> someValue()()
          context.report({
            node: node,
            message: "react useState",
            fix: function* (fixer) {
              // TODO avoid collisions with existing "createSignal" in scope
              yield fixer.replaceText(node.callee, "createSignal");

              // replace getters in scope
              // someValue -> someValue()

              // find getter name
              // example: const [someValue, setSomeValue] = React.useState("initial value")
              const getterName = (() => {
                if (
                  node.parent.type == "VariableDeclarator" &&
                  node.parent.id.type == "ArrayPattern" &&
                  node.parent.id.elements.length >= 1
                ) {
                  return node.parent.id.elements[0].name
                }
              })()
              if (getterName) {
                // https://github.com/mysticatea/eslint-utils/blob/master/docs/api/scope-utils.md
                const globalScope = context.getScope();
                const localScope = eslintUtils.getInnermostScope(globalScope, node);
                const variable = eslintUtils.findVariable(localScope, getterName);
                if (variable) {
                  for (const ref of variable.references) {
                    if (
                      ref.identifier.parent.type == "ArrayPattern" &&
                      ref.identifier.parent.parent.type == "VariableDeclarator"
                    ) {
                      // keep the declaration
                      // example: const [counter1, setCounter1] = createSignal(0)
                      continue
                    }
                    // patch the call sites
                    // examples:
                    //   <div>{counter1}</div>
                    //   const increment1 = () => setCounter1(counter1 + 1)
                    yield fixer.insertTextAfter(ref.identifier, "()")
                  }
                }
              }
            },
          });
        }
      },
    };
  },
};
ts-morph

draft of forEachLocalReference.ts

import {
  ForEachDescendantTraversalControl,
  Identifier,
  Node,
  ts,
} from "ts-morph"

export function isScope(node: Node): boolean {
  return (
    node.isKind(ts.SyntaxKind.Block) ||
    node.isKind(ts.SyntaxKind.FunctionDeclaration) ||
    node.isKind(ts.SyntaxKind.FunctionExpression) ||
    node.isKind(ts.SyntaxKind.ArrowFunction) ||
    // TODO more?
    false
  )
}

export function isDeclaration(node: Identifier): boolean {
  const parent = node.getParent()
  if (!parent) return false
  return (
    (
      parent.isKind(ts.SyntaxKind.VariableDeclaration) ||
      parent.isKind(ts.SyntaxKind.Parameter) ||
      parent.isKind(ts.SyntaxKind.BindingElement) ||
      // TODO more?
      false
    ) &&
    parent.getNameNode() == node
  )
}

/**
 * Get the innermost scope which contains a given node
 * @see https://github.com/mysticatea/eslint-utils/blob/master/src/get-innermost-scope.js
 */
export function getInnermostScope(node: Node, initialScope?: Node): Node | undefined {
  if (!initialScope) initialScope = node.getSourceFile()
  const location = node.getPos()
  let scope = initialScope
  initialScope.forEachDescendant(node => {
    if (
      isScope(node) &&
      node.getPos() <= location &&
      location < node.getEnd()
    ) {
      scope = node
    }
  })
  return scope
}

/**
 * Invoke the cbNode callback for each reference in the local scope
 * @see forEachDescendant
 * @see https://github.com/dsherret/ts-morph/issues/1351
 */
export default function forEachLocalReference(node: Identifier,
  cbNode: ((
    node: Node,
    traversal: ForEachDescendantTraversalControl
  ) => void)
) {
  const scope = getInnermostScope(node)
  if (!scope) return
  const name = node.getText()
  scope.forEachDescendant((node, traversal) => {
    if (
      node.isKind(ts.SyntaxKind.Identifier) &&
      node.getText() == name
    ) {
      if (isDeclaration(node)) {
        // name was redeclared
        traversal.skip()
      }
      else {
        cbNode(node, traversal)
      }
    }
  })
}

Describe alternatives you've considered

is implemented by users, ad-hoc or in a plugin

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Identifier#findReferencesAsNodes and the linked finding-references documentation, then review the proposed forEachLocalReference.ts draft. Define the scope and shadowing behavior for a core local-reference API, and verify that references are limited to the intended scope rather than every scope.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
developer-experience, tooling
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.