text: /**
   * @fileoverview Enforce Inferno components to have a shouldComponentUpdate method
   * @author Evgueni Naverniouk
   */
  
  'use strict';
  
  const Components = require('../util/Components');
  const docsUrl = require('../util/docsUrl');
  
  module.exports = {
    meta: {
      docs: {
        description: 'Enforce Inferno components to have a shouldComponentUpdate method',
        category: 'Best Practices',
        recommended: false,
        url: docsUrl('require-optimization')
      },
  
      messages: {
        noShouldComponentUpdate: 'Component is not optimized. Please add a shouldComponentUpdate method.'
      },
  
      schema: [{
        type: 'object',
        properties: {
          allowDecorators: {
            type: 'array',
            items: {
              type: 'string'
            }
          }
        },
        additionalProperties: false
      }]
    },
  
    create: Components.detect((context, components, utils) => {
      const configuration = context.options[0] || {};
      const allowDecorators = configuration.allowDecorators || [];
  
      /**
       * Checks to see if our component is decorated by PureRenderMixin via infernoMixin
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if node is decorated with a PureRenderMixin, false if not.
       */
      function hasPureRenderDecorator(node) {
        if (node.decorators && node.decorators.length) {
          for (let i = 0, l = node.decorators.length; i < l; i++) {
            if (
              node.decorators[i].expression
              && node.decorators[i].expression.callee
              && node.decorators[i].expression.callee.object
              && node.decorators[i].expression.callee.object.name === 'infernoMixin'
              && node.decorators[i].expression.callee.property
              && node.decorators[i].expression.callee.property.name === 'decorate'
              && node.decorators[i].expression.arguments
              && node.decorators[i].expression.arguments.length
              && node.decorators[i].expression.arguments[0].name === 'PureRenderMixin'
            ) {
              return true;
            }
          }
        }
  
        return false;
      }
  
      /**
       * Checks to see if our component is custom decorated
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if node is decorated name with a custom decorated, false if not.
       */
      function hasCustomDecorator(node) {
        const allowLength = allowDecorators.length;
  
        if (allowLength && node.decorators && node.decorators.length) {
          for (let i = 0; i < allowLength; i++) {
            for (let j = 0, l = node.decorators.length; j < l; j++) {
              if (
                node.decorators[j].expression
                && node.decorators[j].expression.name === allowDecorators[i]
              ) {
                return true;
              }
            }
          }
        }
  
        return false;
      }
  
      /**
       * Checks if we are declaring a shouldComponentUpdate method
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if we are declaring a shouldComponentUpdate method, false if not.
       */
      function isSCUDeclared(node) {
        return Boolean(
          node
          && node.name === 'shouldComponentUpdate'
        );
      }
  
      /**
       * Checks if we are declaring a PureRenderMixin mixin
       * @param {ASTNode} node The AST node being checked.
       * @returns {Boolean} True if we are declaring a PureRenderMixin method, false if not.
       */
      function isPureRenderDeclared(node) {
        let hasPR = false;
        if (node.value && node.value.elements) {
          for (let i = 0, l = node.value.elements.length; i < l; i++) {
            if (node.value.elements[i] && node.value.elements[i].name === 'PureRenderMixin') {
              hasPR = true;
              break;
            }
          }
        }
  
        return Boolean(
          node
          && node.key.name === 'mixins'
          && hasPR
        );
      }
  
      /**
       * Mark shouldComponentUpdate as declared
       * @param {ASTNode} node The AST node being checked.
       */
      function markSCUAsDeclared(node) {
        components.set(node, {
          hasSCU: true
        });
      }
  
      /**
       * Reports missing optimization for a given component
       * @param {Object} component The component to process
       */
      function reportMissingOptimization(component) {
        context.report({
          node: component.node,
          messageId: 'noShouldComponentUpdate'
        });
      }
  
      /**
       * Checks if we are declaring function in class
       * @returns {Boolean} True if we are declaring function in class, false if not.
       */
      function isFunctionInClass() {
        let blockNode;
        let scope = context.getScope();
        while (scope) {
          blockNode = scope.block;
          if (blockNode && blockNode.type === 'ClassDeclaration') {
            return true;
          }
          scope = scope.upper;
        }
  
        return false;
      }
  
      return {
        ArrowFunctionExpression(node) {
          // Skip if the function is declared in the class
          if (isFunctionInClass()) {
            return;
          }
          // Stateless Functional Components cannot be optimized (yet)
          markSCUAsDeclared(node);
        },
  
        ClassDeclaration(node) {
          if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) {
            return;
          }
          markSCUAsDeclared(node);
        },
  
        FunctionDeclaration(node) {
          // Skip if the function is declared in the class
          if (isFunctionInClass()) {
            return;
          }
          // Stateless Functional Components cannot be optimized (yet)
          markSCUAsDeclared(node);
        },
  
        FunctionExpression(node) {
          // Skip if the function is declared in the class
          if (isFunctionInClass()) {
            return;
          }
          // Stateless Functional Components cannot be optimized (yet)
          markSCUAsDeclared(node);
        },
  
        MethodDefinition(node) {
          if (!isSCUDeclared(node.key)) {
            return;
          }
          markSCUAsDeclared(node);
        },
  
        ObjectExpression(node) {
          // Search for the shouldComponentUpdate declaration
          const found = node.properties.some((property) => (
            property.key
            && (isSCUDeclared(property.key) || isPureRenderDeclared(property))
          ));
          if (found) {
            markSCUAsDeclared(node);
          }
        },
  
        'Program:exit'() {
          const list = components.list();
  
          for (let i = 0; i < list.length; i++) {
            const component = list[i];
  
            // Report missing shouldComponentUpdate for all components
            if (!component.hasSCU) {
              reportMissingOptimization(component);
            }
          }
        }
      };
    })
  };
  