Improvements
bendtherules opened this issue ยท 17 comments
General discussion about what more to add and todo items -
TODO items -
- Check if an identifier is referenced
- Is there a way to completely halt traversal?
- Replacing a node with multiple nodes
- Inserting a sibling node
- Pushing a variable declaration to a parent scope
- Checking if a local variable is referenced (i think we can use symbols for this?)
- Rename a binding and its references
- Evaluating expressions
- Throwing a syntax error to ease the developer experience
- Following imports when type checking is disabled
So, let's talk about identifier referencing -
Understanding - You want to find out if the same identifier (i.e. symbol) is being used in two different places.
Example -
let a = 1; // a1
{
let a = "asd"; // a2
}
console.log(a); // a3
Expected - a1
and a3
is same, a2
is different
Link - https://ts-ast-viewer.com/#code/DYUwLgBAhhC8EEYDcAoA3iiWKkjeARFAM4AmBqAvigMYD2AdsXaAHTB0DmAFFAJRIgA
Now if we look at the same code in TAV (with "show internals" on) - select identifer a1 and a3 and notice - their identifier id is different, but the symbol id is same for both (and diferent for a2).
So, solution is we have to look at the symbols and check if they are the same. Either ts has helper for this or their reference will be same or we can just check the id of those symbols. Need to figure out the exact code for this
Edit - Ok, so found it from my old code. They have the same reference (symbols). So, just check if symbolA === symbolB
to find same identifier.
Edit - Added a feature request on TAV for same thing - dsherret/ts-ast-viewer#71. It would be nice to just see it there only.
hey dude thanks for making the issue ๐
nice info for the same symbol usage! are they considered the same node so shallow equality works?
want to make a PR up for adding this info + writing up an example code (in the examples folder)?
Their symbols are considered the same (by reference), identifier is different.
Will raise the PR for this.
Edit: Added.
About "Is there a way to completely halt traversal?" -
You will stop visiting the child nodes if you just stop calling ts.visitEachChild
after visiting the required nodes. But yes, it will still traverse its siblings.
If you do this outside of a transformer, no problem. But inside a transformer, it will cut off all the child nodes from output ast - which is probably not intended. So, instead you can continue visiting the child nodes with a echo function (same output node as input) to "skip" them.
The question is feels little ambiguous to me. What did you exactly mean here?
About "Is there a way to completely halt traversal?"
its basically a part ripped from the babel plugin handbook - in babel you can finish traversing a node by called path.skip()
- which is equivalent in ts as just returning the node instead of calling visitEachChild
however babel also allows you to call path.stop()
- which from my understanding stops traversing all nodes?
is there any equivalent for typescript transformers?
yeah basically - i can't imagine with the current structure of transformers it can work tbh (unless we wrote our own way to traverse children)
Let's look at "Inserting a sibling node" and "Replacing a node with multiple node" -
They are both effectively same, no? To insert a sibling, just return multiple nodes (containing itself plus sibling).
Interesting thing I just noticed in typedef file -
export type Visitor = (node: Node) => VisitResult<Node>;
export type VisitResult<T extends Node> = T | T[] | undefined;
Does that mean our visitor could just return an array? ๐ง๐
Will try this out
They are both effectively same, no?
true!
Does that mean our visitor could just return an array? ๐ง๐
cool!! exciting
About "Rename a binding and its references" -
I understand this is same as renaming a symbol, right? Like the refactor that VS Code offers. So, I was trying to do this (#5) and noticed few things.
First of all, we should probably talk about a marking phase and then modification phase. Why separate marking/finding phase?
-
Because you might have already crossed the to-be-replaced node and then after looking at a later node, understood that some nodes similar to this one (which we might have already crossed) needs to be replaced.
-
If you are doing modifications while checking a node (say change identifiers with name foo to bar), you are basically doing multiple (read+write) operations over and over again (even though logically its a atomic operation). Something like read1, write1, read2, write2, etc. What this means is that write1 might end up affecting read2, which is probably not what you want. You want to check during the transform, that if a identifier was originally called foo rather than if its called foo now in the middle of the transformation. You would want to do all reads first and then all writes (read1, read2, write1, write2).
Think of it as basically modifying or deleting elements of an array while doing foreach over it. Jus to repeat, we don't create a new sourcefile usually in transformation, but modify a existing one - which is like foreach rather than map. So just like in an array, you would usually "find" the interesting elements in a separate sweep and replace them in a separate sweep.
How to do? There is this util ts.forEachChild
- which doesnt need to return any node. Its more like the predicate in Array.find
- if you return a truthy value from the predicate, that is returned, or else keep finding. This can be used to visit all nodes and return + break when you find the interesting node. From our prev discussion, this is basically like skipping other nodes - but only for search, not replacement.
What do you think? We should document this in a separate part or this sounds too complex? ๐
I understand this is same as renaming a symbol, right
yeah exactly. we can update the terminology if it's wrong in the handbook :)
having the two phases sounds right - i don't think its something we could safely do without. it's a shame ts doesn't come with a helper for this - as it seems in babel you can just refer to a path and do an easy path.scope.rename("n");
and boom its done!
if this is all we have right now then it is what it is - i reckon add it both the renaming, and then education of why two traversals might be needed in some situations ๐
So, i think common operations like rename symbol, find references are available in ts language service.
But i am not sure if we can actually use it in a transformer. Which brings us to a bigger question - because there are many things that you can do with compiler api (not just transformers), do you consider them to be within the scope of the document?
Should we add that? For ex, typechecker gives you access to types, language service for common operations like rename, etc. I think these are very closedly linked and this doc at its current state also helps this other usecases (say building a external tool like typedoc for ex).
Maybe we can show the manual process for transformers, but also show easier options for external tools? Basically, focus more on the transformer part but don't exclude other related things from typescript api.
the scope definitely should just be things that can be done with transformers - but if there are opportunities to teach tangentially related concepts/apis why not i guess? this whole handbook exists to try and make learning a bit easier.
if it's mainly comparing transformer vs. language service (plugins?) what's your thoughts if we make expander sections for that extra content? or if its small enough i suppose we wouldn't need it?
would love to see a PR up with the content you're thinking to add ๐
Yeah, maybe we can show a Implementation with just transformer in the main section, then also mention that its possible with lang service and link to a separate section.
TBH, i haven't used lang service but need to learn it. Will add that bit later. For ex, this video shows using "find all references" from that service (https://youtu.be/WlSHiOsl7-U time 22:00).
It will be great to add example about identifying what is imported - type value or runtime value
// source.ts
import { FC, useCallback } from 'react';
// transform.ts (just example)
ts.isTypeValue(FC) // true
ts.isRuntimeValue(useCallback) // true
if you have typechecking turned on you should be able to interrogate the resulting symbol to know :) https://github.com/madou/typescript-transformer-handbook#following-module-imports
unsure if there are helper utils to determine it. if there are or you have suggestions of how to present this throw a PR up ๐
@Madou First of all thanks for this amazing repo. It's a great starting point to get started with transformers in typescript.
It seems like to do follow import we need to use Program factory pattern. In this follow-imports you use ttsc
to run that pattern. I am not using ttsc
in my code and having hard time running this example.
import ts from "typescript";
const filePath = process.argv.slice(2);
const program = ts.createProgram(filePath, {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS
});
const transformerProgram = (program: ts.Program) => {
const transformerFactory: ts.TransformerFactory<ts.SourceFile> = context => {
return sourceFile => {
const visitor = (node: ts.Node): ts.Node => {
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
const typeChecker = program.getTypeChecker();
const importSymbol = typeChecker.getSymbolAtLocation(node.moduleSpecifier);
const exportSymbols = typeChecker.getExportsOfModule(importSymbol);
exportSymbols.forEach(symbol =>
console.log(
`found "${
symbol.escapedName
}" export with value "${symbol.valueDeclaration.getText()}"`
)
);
return node;
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sourceFile, visitor);
};
};
return transformerFactory;
};
// Argument of type program is not assignable to Node | Node[]
ts.transform(program, [transformerProgram]);
Is there any other API to run this pattern? How do we use it without ttsc?