Search code examples
javascripttypescripteslinttslint

Force to store getter result in variable if it is used inside of the loop


Consider following class

class Smth {
  get functionWithSomeVeryUniqueName() {
    // Some heavy calculations are here
    return obj => obj; // Actually uses some vars from closure calculated above
  }
}

I want to have a tslint error on any access to getter inside of the loop.

I. e. any of following lines should be considered bad:

for (var x of a) smth.functionWithSomeVeryUniqueName(x);
a.forEach(x => smth.functionWithSomeVeryUniqueName(x))
a.map(x => smth.functionWithSomeVeryUniqueName(x))
for (var q=0; q<a.length; ++q) smth.functionWithSomeVeryUniqueName(x);

and any of the following - good:

var functionWithSomeVeryUniqueName = smth.functionWithSomeVeryUniqueName;
for (var x of a) functionWithSomeVeryUniqueName(x);
a.forEach(x => functionWithSomeVeryUniqueName(x))
a.map(x => functionWithSomeVeryUniqueName(x))
for (var q=0; q<a.length; ++q) functionWithSomeVeryUniqueName(x);

And this one is good too as the argument is calculated only once:

a.map(smth.functionWithSomeVeryUniqueName)

And call ouside of the loop should be valid:

var x = smth.functionWithSomeVeryUniqueName(mySingleObject)

What tslint rule can be configuref to do such thing?

Note that name check and dot in access are enough, I don' need to ensure that function belongs to some concrete class.


Solution

  • Seems like I've made corresponding rule (demo on AstExplorer):

    import * as Lint from "tslint";
    import * as ts from "typescript";
    
    const arrayMethods = new Set(["find", "findIndex", "sort", "forEach", "filter", "flatMap", "map", "every", "some", "reduce", "reduceRight"]);
    
    export class Rule extends Lint.Rules.AbstractRule {
      public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[]  {
        return this.applyWithWalker(new DisallowGetterInsideOfTheLoopWalker(sourceFile, this.getOptions()));
      }
    }
    
    class DisallowGetterInsideOfTheLoopWalker extends Lint.RuleWalker {
      private loops = 0;
      private names: Set<string>;
    
      constructor(sourceFile, options) {
        super(sourceFile, options);
        this.loops = 0;
        this.names = new Set(["functionWithSomeVeryUniqueName"] /* options.ruleArguments */);
      }
    
      public visitCallExpression(node: ts.CallExpression) {
        var isLoop = node.expression.kind === ts.SyntaxKind.PropertyAccessExpression && arrayMethods.has(node.expression.name.text);
    
        this.loops += isLoop as any;
        super.visitPropertyAccessExpression(node);
        this.loops -= isLoop as any;
      }
    
      public visitPropertyAccessExpression(node: ts.PropertyAccessExpression) {
        if (this.loops && this.names.has(node.name.text) && (this.loops > 1 || node.parent.kind === ts.SyntaxKind.CallExpression && node.parent.expression === node)) {
          this.addFailure(this.createFailure(node.name.pos, node.name.end - node.name.pos, `Do not get ${node.name.text} inside of the loop`));
        }
    
        super.visitPropertyAccessExpression(node);
      }
    
      public visitForOfStatement(node: ts.ForOfStatement) {
        this.loops += 2;
        super.visitForOfStatement(node);
        this.loops -= 2;
      }
    
      public visitForInStatement(node: ts.ForInStatement) {
        this.loops += 2;
        super.visitForInStatement(node);
        this.loops -= 2;
      }
    
      public visitForStatement(node: ts.ForStatement) {
        this.loops += 2;
        super.visitForStatement(node);
        this.loops -= 2;
      }
    
      public visitDoStatement(node: ts.DoStatement) {
        this.loops += 2;
        super.visitDoStatement(node);
        this.loops -= 2;
      }
    
      public visitWhileStatement(node: ts.WhileStatement) {
        this.loops += 2;
        super.visitWhileStatement(node);
        this.loops -= 2;
      }
    }
    

    Invalid cases:

    for (var x of a) smth.functionWithSomeVeryUniqueName(x);
    for (var x in obj) smth.functionWithSomeVeryUniqueName(x);
    a.forEach(x => smth.functionWithSomeVeryUniqueName(x))
    a.map(x => smth.functionWithSomeVeryUniqueName(x))
    a.map(x => smth.functionWithSomeVeryUniqueName<T>(x))
    for (var q=0; q<a.length; ++q) smth.functionWithSomeVeryUniqueName(x);
    do smth.functionWithSomeVeryUniqueName(x); while (0)
    while (1) smth.functionWithSomeVeryUniqueName(x);
    while (1) (smth.functionWithSomeVeryUniqueName)(x);
    while (1) var f = smth.functionWithSomeVeryUniqueName;
    while (1) (smth as any).functionWithSomeVeryUniqueName;
    

    Valid cases:

    var functionWithSomeVeryUniqueName = smth.functionWithSomeVeryUniqueName;
    
    for (var x of a) functionWithSomeVeryUniqueName(x);
    for (var x in obj) functionWithSomeVeryUniqueName(x);
    a.forEach(x => functionWithSomeVeryUniqueName(x))
    a.map(x => functionWithSomeVeryUniqueName(x))
    for (var q=0; q<a.length; ++q) functionWithSomeVeryUniqueName(x);
    do functionWithSomeVeryUniqueName(x); while (0)
    while (1) functionWithSomeVeryUniqueName(x);
    while (1) (functionWithSomeVeryUniqueName)(x);
    
    a.map(smth.functionWithSomeVeryUniqueName)
    smth.functionWithSomeVeryUniqueName(mySingleObject)
    

    Unwanted valid cases:

    a.map(x => (smth.functionWithSomeVeryUniqueName)(x))