microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.17k stars 12.38k forks source link

Compiler API: no Symbol for Node #13165

Open aiboost opened 7 years ago

aiboost commented 7 years ago

TypeScript Version: 2.1.1

Code

I'm trying to implement function detecting is identifier internal (can be renamed) or external but sometimes TS Compiler API returns me no symbols for identifiers:

private isIdentExternal(node: ts.Node): boolean {
    // node here is only ts.Node with kind=65 (Identifier), and already checked not to be reserved word
    let symbol = this.program.getTypeChecker().getSymbolAtLocation(node);
    if (!symbol) {
        // why?
    }
    for (const decl of symbol.declarations)
      if (decl.getSourceFile().fileName.endsWith(".d.ts"))
        return true;
    return false;
  }

There are two known cases when I get this problem.

Case 1: no symbol for internal abstract function:

abstract class Cls {
    private abstract func();// no symbol is here
    // Tough I can get symbols from cls.func() code in other place, the problem is for declaration only
}

Case 2: no symbol for external properties:

let el: HTMLElement = document.createElement("div");
el.className = "x"; // no symbol for "className", tough I have symbol for "createElement" (problem is only for properties)

Expected behavior:

Every identifier (function/variable/method/property/class definition) has symbol.

Actual behavior:

Sometimes symbol is missing (TypeChecker does not return it).

aiboost commented 7 years ago

Any hint? Please tell me where I am wrong and is this actually a bug? I've found some more cases with no Node->Symbol conversion, and do not understand how can it be possible. I need it to write custom obfuscator for my app.

voldmar commented 7 years ago

Could you also give SourceFile to your function as argument and use it?

aiboost commented 7 years ago

@voldmar , yes, I can pass SourceFile there but this will be source file of place where the call exists, not where the definition is made.

For example in the code el.className SourceFile will be always my source .ts, but the symbol className is defined in "es5.d.ts". In case of methods defined there (e. g. "appendChild()") I can get Symbol fine and it has declaration array containing "es5.d.ts" inside, but I fail on properties (case 2).

voldmar commented 7 years ago

Hm, I just have used ts.ModuleKind.ES2015 for module type and got proper el.className resolution

aiboost commented 7 years ago

@voldmar , I have these options for compiler:

const options: ts.CompilerOptions = {
  allowNonTsExtensions: true,
  module: ts.ModuleKind.AMD,
  target: ts.ScriptTarget.ES5,
};

ES5 is passed to compiler fine (methods have symbols). Looks like maybe I need to play with compiler version... In any case thank you for advice.

pronebird commented 5 years ago

@RyanCavanaugh any progress on this? I have a similar issue in a custom transformer that replaces all calls to process.env.NODE_ENV with a string literal. Basically getSymbolAtLocation works very well but there are edge cases where it's doesn't return any info, for instance it returns undefined for process['env']['NODE_ENV'], but works for process.env.NODE_ENV...

const ts = require('typescript');

module.exports = function (program) {
  const typeChecker = program.getTypeChecker();

  return function (context) {
    const visit = (node) => {
      const isPropertyExpr = ts.isPropertyAccessExpression(node) && node.name.text === 'NODE_ENV';
      const isElementExpr = ts.isElementAccessExpression(node) && node.argumentExpression.text === 'NODE_ENV';

      if (isPropertyExpr || isElementExpr) {
        const symbol = typeChecker.getSymbolAtLocation(node.expression);

        if (!symbol) {
          throw new Error('No symbol for ' + node.getFullText());
        }

        if (symbol && symbol.getName() === 'env' && symbol.parent && symbol.parent.getName() === 'Process') {
          return ts.createLiteral('production');
        }
      }

      return ts.visitEachChild(node, (child) => visit(child), context);
    };
    return (node) => ts.visitNode(node, visit);
  };
};
foxel commented 3 years ago

Any update on this issue? I'm facing the same while implementing a custom rule for tslint.

lolleko commented 3 years ago

Also facing a similar issue. Would appreciate an update.

Perryvw commented 3 years ago

Also ran into this today. Surprisingly, while comparing two functions in the same source file, one of them has a symbol and the other does not.

function startParty() does not have a symbol in the compiler API.

A few lines down, function stopParty() has a symbol as expected.

These functions are both pretty simple and very similar, making this bug even weirder. We have tried but were not able to create a smaller reproduction.

Edit: Figured out it might have something to do with the transformer plugin we had enabled in our tsconfig. Will investigate further tomorrow and let you know what I find.

DanielSWolf commented 2 years ago

I'm experiencing the same problem trying to get the symbol for a variable declaration. I tried getSymbolAtLocation() with the VariableDeclaration, the VariableStatement, and, out of sheer desperation, the VariableDeclarationList. In each case, I only got undefined.

I'm not sure if that's any help, but the AST explorer shows the same behavior. I just typed let foo: number; and expanded the AST. Under VariableDeclaration, it shows symbol: undefined (see screenshot). Is there some option I failed to set?

image

mhw0 commented 1 year ago

@DanielSWolf I was facing the same issue. I did some investigation and It seems like in order to get getSymbolAtLocation() to work you should pass the name property of the declaration, not the declaration itself.

The source code of getSymbolAtLocation() clearly shows that it doesn't expect the node.kind to be any declaration, but it does accept Identifiers and QualifiedNames.