finding references in local scope
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
- getInnermostScope to get from declaration to scope (#646)
- findVariable to get from scope to reference
// 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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