/**
 * @fileoverview Ensures that definitions and uses of properties on the
 * ``lazy`` object are valid.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

"use strict";
const helpers = require("../helpers");

const items = [
  "loader",
  "XPCOMUtils",
  "Integration",
  "ChromeUtils",
  "DevToolsUtils",
  "Object",
  "Reflect",
];

const callExpressionDefinitions = [
  /^loader\.lazyGetter\(lazy, "(\w+)"/,
  /^loader\.lazyServiceGetter\(lazy, "(\w+)"/,
  /^loader\.lazyRequireGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyGetter\(lazy, "(\w+)"/,
  /^Integration\.downloads\.defineESModuleGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyModuleGetter\(lazy, "(\w+)"/,
  /^ChromeUtils\.defineLazyGetter\(lazy, "(\w+)"/,
  /^ChromeUtils\.defineModuleGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyPreferenceGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyProxy\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyScriptGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineLazyServiceGetter\(lazy, "(\w+)"/,
  /^XPCOMUtils\.defineConstant\(lazy, "(\w+)"/,
  /^DevToolsUtils\.defineLazyModuleGetter\(lazy, "(\w+)"/,
  /^DevToolsUtils\.defineLazyGetter\(lazy, "(\w+)"/,
  /^Object\.defineProperty\(lazy, "(\w+)"/,
  /^Reflect\.defineProperty\(lazy, "(\w+)"/,
];

const callExpressionMultiDefinitions = [
  "ChromeUtils.defineESModuleGetters(lazy,",
  "XPCOMUtils.defineLazyModuleGetters(lazy,",
  "XPCOMUtils.defineLazyServiceGetters(lazy,",
  "Object.defineProperties(lazy,",
  "loader.lazyRequireGetter(lazy,",
];

module.exports = {
  meta: {
    docs: {
      url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/valid-lazy.html",
    },
    messages: {
      duplicateSymbol: "Duplicate symbol {{name}} being added to lazy.",
      incorrectType: "Unexpected literal for property name {{name}}",
      unknownProperty: "Unknown lazy member property {{name}}",
      unusedProperty: "Unused lazy property {{name}}",
      topLevelAndUnconditional:
        "Lazy property {{name}} is used at top-level unconditionally. It should be non-lazy.",
    },
    schema: [],
    type: "problem",
  },

  create(context) {
    let lazyProperties = new Map();
    let unknownProperties = [];

    function addProp(node, name) {
      if (lazyProperties.has(name)) {
        context.report({
          node,
          messageId: "duplicateSymbol",
          data: { name },
        });
        return;
      }
      lazyProperties.set(name, { used: false, node });
    }

    function setPropertiesFromArgument(node, arg) {
      if (arg.type === "ObjectExpression") {
        for (let prop of arg.properties) {
          if (prop.key.type == "Literal") {
            context.report({
              node,
              messageId: "incorrectType",
              data: { name: prop.key.value },
            });
            continue;
          }
          addProp(node, prop.key.name);
        }
      } else if (arg.type === "ArrayExpression") {
        for (let prop of arg.elements) {
          if (prop.type != "Literal") {
            continue;
          }
          addProp(node, prop.value);
        }
      }
    }

    return {
      VariableDeclarator(node) {
        if (
          node.id.type === "Identifier" &&
          node.id.name == "lazy" &&
          node.init.type == "CallExpression" &&
          node.init.callee.name == "createLazyLoaders"
        ) {
          setPropertiesFromArgument(node, node.init.arguments[0]);
        }
      },

      CallExpression(node) {
        if (
          node.callee.type != "MemberExpression" ||
          (node.callee.object.type == "MemberExpression" &&
            !items.includes(node.callee.object.object.name)) ||
          (node.callee.object.type != "MemberExpression" &&
            !items.includes(node.callee.object.name))
        ) {
          return;
        }

        let source;
        try {
          source = helpers.getASTSource(node);
        } catch (e) {
          return;
        }

        for (let reg of callExpressionDefinitions) {
          let match = source.match(reg);
          if (match) {
            if (lazyProperties.has(match[1])) {
              context.report({
                node,
                messageId: "duplicateSymbol",
                data: { name: match[1] },
              });
              return;
            }
            lazyProperties.set(match[1], { used: false, node });
            break;
          }
        }

        if (
          callExpressionMultiDefinitions.some(expr =>
            source.startsWith(expr)
          ) &&
          node.arguments[1]
        ) {
          setPropertiesFromArgument(node, node.arguments[1]);
        }
      },

      MemberExpression(node) {
        if (node.computed || node.object.type !== "Identifier") {
          return;
        }

        let name;
        if (node.object.name == "lazy") {
          name = node.property.name;
        } else {
          return;
        }
        let property = lazyProperties.get(name);
        if (!property) {
          // These will be reported on Program:exit - some definitions may
          // be after first use, so we need to wait until we've processed
          // the whole file before reporting.
          unknownProperties.push({ name, node });
        } else {
          property.used = true;
        }
        if (
          helpers.getIsTopLevelAndUnconditionallyExecuted(
            context.getAncestors()
          )
        ) {
          context.report({
            node,
            messageId: "topLevelAndUnconditional",
            data: { name },
          });
        }
      },

      "Program:exit": function () {
        for (let { name, node } of unknownProperties) {
          let property = lazyProperties.get(name);
          if (!property) {
            context.report({
              node,
              messageId: "unknownProperty",
              data: { name },
            });
          } else {
            property.used = true;
          }
        }
        for (let [name, property] of lazyProperties.entries()) {
          if (!property.used) {
            context.report({
              node: property.node,
              messageId: "unusedProperty",
              data: { name },
            });
          }
        }
      },
    };
  },
};
