/**
 * @fileoverview Rule to disallow empty functions.
 * @author Toru Nagashima
 */

"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const astUtils = require("./utils/ast-utils");

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

const ALLOW_OPTIONS = Object.freeze([
	"functions",
	"arrowFunctions",
	"generatorFunctions",
	"methods",
	"generatorMethods",
	"getters",
	"setters",
	"constructors",
	"asyncFunctions",
	"asyncMethods",
]);

/**
 * Gets the kind of a given function node.
 * @param {ASTNode} node A function node to get. This is one of
 *      an ArrowFunctionExpression, a FunctionDeclaration, or a
 *      FunctionExpression.
 * @returns {string} The kind of the function. This is one of "functions",
 *      "arrowFunctions", "generatorFunctions", "asyncFunctions", "methods",
 *      "generatorMethods", "asyncMethods", "getters", "setters", and
 *      "constructors".
 */
function getKind(node) {
	const parent = node.parent;
	let kind;

	if (node.type === "ArrowFunctionExpression") {
		return "arrowFunctions";
	}

	// Detects main kind.
	if (parent.type === "Property") {
		if (parent.kind === "get") {
			return "getters";
		}
		if (parent.kind === "set") {
			return "setters";
		}
		kind = parent.method ? "methods" : "functions";
	} else if (parent.type === "MethodDefinition") {
		if (parent.kind === "get") {
			return "getters";
		}
		if (parent.kind === "set") {
			return "setters";
		}
		if (parent.kind === "constructor") {
			return "constructors";
		}
		kind = "methods";
	} else {
		kind = "functions";
	}

	// Detects prefix.
	let prefix;

	if (node.generator) {
		prefix = "generator";
	} else if (node.async) {
		prefix = "async";
	} else {
		return kind;
	}
	return prefix + kind[0].toUpperCase() + kind.slice(1);
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../shared/types').Rule} */
module.exports = {
	meta: {
		type: "suggestion",

		defaultOptions: [{ allow: [] }],

		docs: {
			description: "Disallow empty functions",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/no-empty-function",
		},

		schema: [
			{
				type: "object",
				properties: {
					allow: {
						type: "array",
						items: { enum: ALLOW_OPTIONS },
						uniqueItems: true,
					},
				},
				additionalProperties: false,
			},
		],

		messages: {
			unexpected: "Unexpected empty {{name}}.",
		},
	},

	create(context) {
		const [{ allow }] = context.options;
		const sourceCode = context.sourceCode;

		/**
		 * Reports a given function node if the node matches the following patterns.
		 *
		 * - Not allowed by options.
		 * - The body is empty.
		 * - The body doesn't have any comments.
		 * @param {ASTNode} node A function node to report. This is one of
		 *      an ArrowFunctionExpression, a FunctionDeclaration, or a
		 *      FunctionExpression.
		 * @returns {void}
		 */
		function reportIfEmpty(node) {
			const kind = getKind(node);
			const name = astUtils.getFunctionNameWithKind(node);
			const innerComments = sourceCode.getTokens(node.body, {
				includeComments: true,
				filter: astUtils.isCommentToken,
			});

			if (
				!allow.includes(kind) &&
				node.body.type === "BlockStatement" &&
				node.body.body.length === 0 &&
				innerComments.length === 0
			) {
				context.report({
					node,
					loc: node.body.loc,
					messageId: "unexpected",
					data: { name },
				});
			}
		}

		return {
			ArrowFunctionExpression: reportIfEmpty,
			FunctionDeclaration: reportIfEmpty,
			FunctionExpression: reportIfEmpty,
		};
	},
};
