Skip to content

⬅️ Back to Table of Contents

πŸ“„ no-unnecessary-qualifier

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 12
πŸ“¦ Imports 4

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/no-unnecessary-qualifier.ts

πŸ“€ Default Export

export default createRule({ ... })
Property Value
name 'no-unnecessary-qualifier'
meta.type 'suggestion'
meta.docs.description 'Disallow unnecessary namespace qualifiers'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.unnecessaryQualifier "Qualifier is unnecessary since '{{ name }}' is in scope."
meta.schema []
defaultOptions []

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getParserServices ../util

Functions

create(context: any): { 'ExportNamedDeclaration[declaration.type="TSEnumDeclarati…

Parameters:

  • context any

Returns: { 'ExportNamedDeclaration[declaration.type="TSEnumDeclaration"]': (node: TSESTree.ExportNamedDeclaration | TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration) => void; 'ExportNamedDeclaration[declaration.type="TSEnumDeclaration"]:exit': () => void; 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]': (node: TSESTree.ExportNamedDeclaration | TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration) => void; 'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]:exit': () => void; 'MemberExpression:exit': (node: TSESTree.Node) => void; 'MemberExpression[computed=false]'(node: TSESTree.MemberExpression): void; TSEnumDeclaration: (node: TSESTree.ExportNamedDeclaration | TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration) => void; 'TSEnumDeclaration:exit': () => void; 'TSModuleDeclaration:exit': () => void; 'TSModuleDeclaration > TSModuleBlock'(node: TSESTree.TSModuleBlock): void; TSQualifiedName(node: TSESTree.TSQualifiedName): void; 'TSQualifiedName:exit': (node: TSESTree.Node) => void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • tsutils.isSymbolFlagSet
  • checker.getAliasedSymbol
  • symbol.getDeclarations
  • symbolDeclarations.some
  • namespacesInScope.some
  • tryGetAliasedSymbol
  • symbolIsNamespaceInScope
  • checker.getSymbolsInScope
  • scope.find
  • checker.getExportSymbolOfSymbol
  • services.getSymbolAtLocation
  • esTreeNodeToTSNodeMap.get
  • getSymbolInScope
  • context.sourceCode.getText
  • symbolsAreEqual
  • qualifierIsUnnecessary
  • context.report
  • fixer.removeRange
  • namespacesInScope.push
  • namespacesInScope.pop
  • isPropertyAccessExpression
  • isEntityNameExpression
  • visitNamespaceAccess
  • enterDeclaration

Internal Comments:

// If the symbol in scope is different, the qualifier is necessary. (x2)
// Only look for nested qualifier errors if we didn't already fail on the outer qualifier.

Code
create(context) {
    const namespacesInScope: ts.Node[] = [];
    let currentFailedNamespaceExpression: TSESTree.Node | null = null;
    const services = getParserServices(context);
    const esTreeNodeToTSNodeMap = services.esTreeNodeToTSNodeMap;
    const checker = services.program.getTypeChecker();

    function tryGetAliasedSymbol(
      symbol: ts.Symbol,
      checker: ts.TypeChecker,
    ): ts.Symbol | null {
      return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias)
        ? checker.getAliasedSymbol(symbol)
        : null;
    }

    function symbolIsNamespaceInScope(symbol: ts.Symbol): boolean {
      const symbolDeclarations = symbol.getDeclarations() ?? [];

      if (
        symbolDeclarations.some(decl =>
          namespacesInScope.some(ns => ns === decl),
        )
      ) {
        return true;
      }

      const alias = tryGetAliasedSymbol(symbol, checker);

      return alias != null && symbolIsNamespaceInScope(alias);
    }

    function getSymbolInScope(
      node: ts.Node,
      flags: ts.SymbolFlags,
      name: string,
    ): ts.Symbol | undefined {
      const scope = checker.getSymbolsInScope(node, flags);

      return scope.find(scopeSymbol => scopeSymbol.name === name);
    }

    function symbolsAreEqual(accessed: ts.Symbol, inScope: ts.Symbol): boolean {
      return accessed === checker.getExportSymbolOfSymbol(inScope);
    }

    function qualifierIsUnnecessary(
      qualifier: TSESTree.EntityName | TSESTree.MemberExpression,
      name: TSESTree.Identifier,
    ): boolean {
      const namespaceSymbol = services.getSymbolAtLocation(qualifier);

      if (
        namespaceSymbol == null ||
        !symbolIsNamespaceInScope(namespaceSymbol)
      ) {
        return false;
      }

      const accessedSymbol = services.getSymbolAtLocation(name);

      if (accessedSymbol == null) {
        return false;
      }

      // If the symbol in scope is different, the qualifier is necessary.
      const tsQualifier = esTreeNodeToTSNodeMap.get(qualifier);
      const fromScope = getSymbolInScope(
        tsQualifier,
        accessedSymbol.flags,
        context.sourceCode.getText(name),
      );

      return !!fromScope && symbolsAreEqual(accessedSymbol, fromScope);
    }

    function visitNamespaceAccess(
      node: TSESTree.Node,
      qualifier: TSESTree.EntityName | TSESTree.MemberExpression,
      name: TSESTree.Identifier,
    ): void {
      // Only look for nested qualifier errors if we didn't already fail on the outer qualifier.
      if (
        !currentFailedNamespaceExpression &&
        qualifierIsUnnecessary(qualifier, name)
      ) {
        currentFailedNamespaceExpression = node;
        context.report({
          node: qualifier,
          messageId: 'unnecessaryQualifier',
          data: {
            name: context.sourceCode.getText(name),
          },
          fix(fixer) {
            return fixer.removeRange([qualifier.range[0], name.range[0]]);
          },
        });
      }
    }

    function enterDeclaration(
      node:
        | TSESTree.ExportNamedDeclaration
        | TSESTree.TSEnumDeclaration
        | TSESTree.TSModuleDeclaration,
    ): void {
      namespacesInScope.push(esTreeNodeToTSNodeMap.get(node));
    }

    function exitDeclaration(): void {
      namespacesInScope.pop();
    }

    function resetCurrentNamespaceExpression(node: TSESTree.Node): void {
      if (node === currentFailedNamespaceExpression) {
        currentFailedNamespaceExpression = null;
      }
    }

    function isPropertyAccessExpression(
      node: TSESTree.Node,
    ): node is TSESTree.MemberExpression {
      return node.type === AST_NODE_TYPES.MemberExpression && !node.computed;
    }

    function isEntityNameExpression(
      node: TSESTree.Node,
    ): node is TSESTree.Identifier | TSESTree.MemberExpression {
      return (
        node.type === AST_NODE_TYPES.Identifier ||
        (isPropertyAccessExpression(node) &&
          isEntityNameExpression(node.object))
      );
    }

    return {
      'ExportNamedDeclaration[declaration.type="TSEnumDeclaration"]':
        enterDeclaration,
      'ExportNamedDeclaration[declaration.type="TSEnumDeclaration"]:exit':
        exitDeclaration,
      'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]':
        enterDeclaration,
      'ExportNamedDeclaration[declaration.type="TSModuleDeclaration"]:exit':
        exitDeclaration,
      'MemberExpression:exit': resetCurrentNamespaceExpression,
      'MemberExpression[computed=false]'(
        node: TSESTree.MemberExpression,
      ): void {
        const property = node.property as TSESTree.Identifier;
        if (isEntityNameExpression(node.object)) {
          visitNamespaceAccess(node, node.object, property);
        }
      },
      TSEnumDeclaration: enterDeclaration,
      'TSEnumDeclaration:exit': exitDeclaration,
      'TSModuleDeclaration:exit': exitDeclaration,
      'TSModuleDeclaration > TSModuleBlock'(
        node: TSESTree.TSModuleBlock,
      ): void {
        enterDeclaration(node.parent);
      },
      TSQualifiedName(node: TSESTree.TSQualifiedName): void {
        visitNamespaceAccess(node, node.left, node.right);
      },
      'TSQualifiedName:exit': resetCurrentNamespaceExpression,
    };
  }

Internal helpers

Declared inside another function in this file.

tryGetAliasedSymbol(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Symbol | null

Parameters:

  • symbol ts.Symbol
  • checker ts.TypeChecker

Returns: ts.Symbol | null

Calls:

  • tsutils.isSymbolFlagSet
  • checker.getAliasedSymbol
Code
function tryGetAliasedSymbol(
      symbol: ts.Symbol,
      checker: ts.TypeChecker,
    ): ts.Symbol | null {
      return tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias)
        ? checker.getAliasedSymbol(symbol)
        : null;
    }

symbolIsNamespaceInScope(symbol: ts.Symbol): boolean

Parameters:

  • symbol ts.Symbol

Returns: boolean

Calls:

  • symbol.getDeclarations
  • symbolDeclarations.some
  • namespacesInScope.some
  • tryGetAliasedSymbol
  • symbolIsNamespaceInScope
Code
function symbolIsNamespaceInScope(symbol: ts.Symbol): boolean {
      const symbolDeclarations = symbol.getDeclarations() ?? [];

      if (
        symbolDeclarations.some(decl =>
          namespacesInScope.some(ns => ns === decl),
        )
      ) {
        return true;
      }

      const alias = tryGetAliasedSymbol(symbol, checker);

      return alias != null && symbolIsNamespaceInScope(alias);
    }

getSymbolInScope(node: ts.Node, flags: ts.SymbolFlags, name: string): ts.Symbol | undefined

Parameters:

  • node ts.Node
  • flags ts.SymbolFlags
  • name string

Returns: ts.Symbol | undefined

Calls:

  • checker.getSymbolsInScope
  • scope.find
Code
function getSymbolInScope(
      node: ts.Node,
      flags: ts.SymbolFlags,
      name: string,
    ): ts.Symbol | undefined {
      const scope = checker.getSymbolsInScope(node, flags);

      return scope.find(scopeSymbol => scopeSymbol.name === name);
    }

symbolsAreEqual(accessed: ts.Symbol, inScope: ts.Symbol): boolean

Parameters:

  • accessed ts.Symbol
  • inScope ts.Symbol

Returns: boolean

Calls:

  • checker.getExportSymbolOfSymbol
Code
function symbolsAreEqual(accessed: ts.Symbol, inScope: ts.Symbol): boolean {
      return accessed === checker.getExportSymbolOfSymbol(inScope);
    }

qualifierIsUnnecessary(qualifier: TSESTree.EntityName | TSESTree.MemberEx…, name: TSESTree.Identifier): boolean

Parameters:

  • qualifier TSESTree.EntityName | TSESTree.MemberExpression
  • name TSESTree.Identifier

Returns: boolean

Calls:

  • services.getSymbolAtLocation
  • symbolIsNamespaceInScope
  • esTreeNodeToTSNodeMap.get
  • getSymbolInScope
  • context.sourceCode.getText
  • symbolsAreEqual

Internal Comments:

// If the symbol in scope is different, the qualifier is necessary. (x2)

Code
function qualifierIsUnnecessary(
      qualifier: TSESTree.EntityName | TSESTree.MemberExpression,
      name: TSESTree.Identifier,
    ): boolean {
      const namespaceSymbol = services.getSymbolAtLocation(qualifier);

      if (
        namespaceSymbol == null ||
        !symbolIsNamespaceInScope(namespaceSymbol)
      ) {
        return false;
      }

      const accessedSymbol = services.getSymbolAtLocation(name);

      if (accessedSymbol == null) {
        return false;
      }

      // If the symbol in scope is different, the qualifier is necessary.
      const tsQualifier = esTreeNodeToTSNodeMap.get(qualifier);
      const fromScope = getSymbolInScope(
        tsQualifier,
        accessedSymbol.flags,
        context.sourceCode.getText(name),
      );

      return !!fromScope && symbolsAreEqual(accessedSymbol, fromScope);
    }

visitNamespaceAccess(node: TSESTree.Node, qualifier: TSESTree.EntityName | TSESTree.MemberEx…, name: TSESTree.Identifier): void

Parameters:

  • node TSESTree.Node
  • qualifier TSESTree.EntityName | TSESTree.MemberExpression
  • name TSESTree.Identifier

Returns: void

Calls:

  • qualifierIsUnnecessary
  • context.report
  • context.sourceCode.getText
  • fixer.removeRange

Internal Comments:

// Only look for nested qualifier errors if we didn't already fail on the outer qualifier.

Code
function visitNamespaceAccess(
      node: TSESTree.Node,
      qualifier: TSESTree.EntityName | TSESTree.MemberExpression,
      name: TSESTree.Identifier,
    ): void {
      // Only look for nested qualifier errors if we didn't already fail on the outer qualifier.
      if (
        !currentFailedNamespaceExpression &&
        qualifierIsUnnecessary(qualifier, name)
      ) {
        currentFailedNamespaceExpression = node;
        context.report({
          node: qualifier,
          messageId: 'unnecessaryQualifier',
          data: {
            name: context.sourceCode.getText(name),
          },
          fix(fixer) {
            return fixer.removeRange([qualifier.range[0], name.range[0]]);
          },
        });
      }
    }

enterDeclaration(node: | TSESTree.ExportNamedDeclaration | TSE…): void

Parameters:

  • node | TSESTree.ExportNamedDeclaration | TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration

Returns: void

Calls:

  • namespacesInScope.push
  • esTreeNodeToTSNodeMap.get
Code
function enterDeclaration(
      node:
        | TSESTree.ExportNamedDeclaration
        | TSESTree.TSEnumDeclaration
        | TSESTree.TSModuleDeclaration,
    ): void {
      namespacesInScope.push(esTreeNodeToTSNodeMap.get(node));
    }

exitDeclaration(): void

Returns: void

Calls:

  • namespacesInScope.pop
Code
function exitDeclaration(): void {
      namespacesInScope.pop();
    }

resetCurrentNamespaceExpression(node: TSESTree.Node): void

Parameters:

  • node TSESTree.Node

Returns: void

Code
function resetCurrentNamespaceExpression(node: TSESTree.Node): void {
      if (node === currentFailedNamespaceExpression) {
        currentFailedNamespaceExpression = null;
      }
    }

isPropertyAccessExpression(node: TSESTree.Node): node is TSESTree.MemberExpression

Parameters:

  • node TSESTree.Node

Returns: node is TSESTree.MemberExpression

Code
function isPropertyAccessExpression(
      node: TSESTree.Node,
    ): node is TSESTree.MemberExpression {
      return node.type === AST_NODE_TYPES.MemberExpression && !node.computed;
    }

isEntityNameExpression(node: TSESTree.Node): node is TSESTree.Identifier | TSESTree.MemberExpression

Parameters:

  • node TSESTree.Node

Returns: node is TSESTree.Identifier | TSESTree.MemberExpression

Calls:

  • isPropertyAccessExpression
  • isEntityNameExpression
Code
function isEntityNameExpression(
      node: TSESTree.Node,
    ): node is TSESTree.Identifier | TSESTree.MemberExpression {
      return (
        node.type === AST_NODE_TYPES.Identifier ||
        (isPropertyAccessExpression(node) &&
          isEntityNameExpression(node.object))
      );
    }

Generated by Syntax Scribe