UNPKG

@adguard/eslint-plugin-logger-context

Version:

ESLint plugin that requires AdGuard logger calls to start with a context tag.

129 lines (118 loc) 4.25 kB
/** * @file * ESLint rule for requiring logger calls to include a context tag * e.g. "[ext.page-handler]:" or "[tsweb.WebRequestApi.onBeforeRequest]:". */ import { ESLintUtils, TSESTree } from '@typescript-eslint/utils'; import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; import path from 'path'; import { LogMethod } from '@adguard/logger'; import { getEnclosingNames, buildTag, startsWithTag, createFix, } from './helpers'; const DEFAULT_CONTEXT_MODULE_NAME = 'logger'; const DEFAULT_LOGGER_VARIABLE_NAME = 'logger'; const UNKNOWN_FILE_NAME = 'unknown'; const FILE_EXTENSIONS: ReadonlySet<string> = new Set([ '.ts', '.js', '.tsx', '.jsx', ]); /** * Options for require-logger-context ESLint rule. */ type Options = [ { /** * Specifies the context module name to use in the tag. Defaults to 'logger'. */ contextModuleName?: string; /** * Specifies the logger variable name to use in the tag. * Used to identify the logger instance variable name in the code to check all logger calls. * Defaults to 'logger'. */ loggerVariableName?: string; }?, ]; type MessageIds = 'missingContextTag'; /** * Extracts the file name from the ESLint context. * * @param context The ESLint rule context. * * @returns The file name, or UNKNOWN_FILE_NAME if not a known extension. */ function getFileName(context: RuleContext<MessageIds, Options>): string { return (FILE_EXTENSIONS.has(path.extname(context.filename))) // Only filename without extension ? path.basename(context.filename).replace(path.extname(context.filename), '') : UNKNOWN_FILE_NAME; } /** * Rule definition for require-logger-context. */ const createRule = ESLintUtils.RuleCreator((ruleName) => (`https://example.com/rules/${ruleName}`)); export const requireLoggerContextRule = createRule<Options, MessageIds>({ name: 'require-logger-context', meta: { type: 'suggestion', docs: { description: 'Require logger calls to include a context tag', }, fixable: 'code', schema: [ { type: 'object', properties: { contextModuleName: { type: 'string' }, loggerVariableName: { type: 'string' }, }, additionalProperties: false, }, ], messages: { missingContextTag: 'Logger calls must start with a context tag, e.g. {{tag}} ...', }, }, defaultOptions: [{}], create(context, [options = {}]) { const contextModuleName = options.contextModuleName ?? DEFAULT_CONTEXT_MODULE_NAME; const loggerVariableName = options.loggerVariableName ?? DEFAULT_LOGGER_VARIABLE_NAME; const logLevelMethods = new Set<string>(Object.values(LogMethod)); return { CallExpression(node: TSESTree.CallExpression): void { if ( node.callee.type !== TSESTree.AST_NODE_TYPES.MemberExpression || node.callee.object.type !== TSESTree.AST_NODE_TYPES.Identifier || node.callee.object.name !== loggerVariableName || node.callee.property.type !== TSESTree.AST_NODE_TYPES.Identifier ) { return; } const calledMethodName = node.callee.property.name; if (!logLevelMethods.has(calledMethodName)) { return; } const fileName = getFileName(context); const { className, methodName } = getEnclosingNames(node); const tag = buildTag(contextModuleName, fileName, className, methodName); const firstArg = node.arguments[0]; if (startsWithTag(firstArg, tag)) { return; } const fix = createFix(context, node, tag); context.report({ node, messageId: 'missingContextTag', data: { tag }, fix, }); }, }; }, });