chromium/ash/webui/camera_app_ui/resources/eslint_plugin/index.js

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Note: https://astexplorer.net/ is useful when developing custom rules.
// Select "JavaScript", "@typescript-eslint/parser", "Transform: ESLint v4"
// on the top options.
//
// Note that the ESLint we're using is v7, so there's some difference. Most
// notably, the node type names we're getting in the rules are same as the one
// in the AST (e.g. "TSTypeAnnotation") instead of the one got in the ESLint
// pass on astexplorer.net (e.g. "TypeAnnotation").

const parameterCommentFormatRule = {
  create: (context) => {
    const sourceCode = context.getSourceCode();
    return {
      /* eslint-disable-next-line @typescript-eslint/naming-convention */
      CallExpression(node) {
        for (const arg of node.arguments) {
          const comments = sourceCode.getComments(arg);
          for (const comment of comments.leading) {
            const {type, value} = comment;
            if (type !== 'Block') {
              continue;
            }
            if (!value.match(/ \w+= /) &&
                !value.match(/^\s*eslint-disable-next-line/)) {
              context.report({
                node: comment,
                message: 'Inline block comment for parameters' +
                    ' should be in the form of /* var= */',
              });
            }
          }
        }
      },
    };
  },
};

const genericParameterOnDeclarationType = {
  create: (context) => {
    function checkTypeParameterSameAsNewExpression(typeAnnotation, value) {
      if (value === null || value.type !== 'NewExpression' ||
          value.callee.type !== 'Identifier') {
        return;
      }
      const newTypeName = value.callee.name;

      if (typeAnnotation === undefined ||
          typeAnnotation.type !== 'TSTypeAnnotation' ||
          typeAnnotation.typeAnnotation.type !== 'TSTypeReference' ||
          typeAnnotation.typeAnnotation.typeName.type !== 'Identifier') {
        return;
      }
      const typeAnnotationTypeName =
          typeAnnotation.typeAnnotation.typeName.name;

      if (newTypeName === typeAnnotationTypeName) {
        if (typeAnnotation.typeAnnotation.typeParameters !== undefined) {
          context.report({
            node: typeAnnotation.typeAnnotation.typeParameters,
            message:
                'Generic type parameters can be moved to the new expression.',
          });
        } else {
          context.report({
            node: typeAnnotation.typeAnnotation,
            message: 'Redundant type annotation.',
          });
        }
      }
    }
    return {
      /* eslint-disable-next-line @typescript-eslint/naming-convention */
      PropertyDefinition({value, typeAnnotation}) {
        checkTypeParameterSameAsNewExpression(typeAnnotation, value);
      },
      /* eslint-disable-next-line @typescript-eslint/naming-convention */
      VariableDeclarator({id: {typeAnnotation}, init}) {
        checkTypeParameterSameAsNewExpression(typeAnnotation, init);
      },
    };
  },
};

const todoFormatRule = {
  create: (context) => {
    function verifyTodo(comment) {
      return !/TODO(?!\((b\/\d+|crbug\.com\/\d+|[a-z]+)\))/g.test(comment);
    }

    const sourceCode = context.getSourceCode();
    return {
      /* eslint-disable-next-line @typescript-eslint/naming-convention */
      Program: function() {
        const comments = sourceCode.getAllComments();
        for (const comment of comments) {
          const commentValue = comment.value;
          if (!verifyTodo(commentValue)) {
            context.report({
              node: comment,
              message: `Use: TODO(ldap) / TODO(b/123456789) /` +
                  ` TODO(crbug.com/123456789)`,
            });
          }
        }
      },
    };
  },
};

const stringEnumOrder = {
  create: (context) => {
    return {
      /* eslint-disable-next-line @typescript-eslint/naming-convention */
      TSEnumDeclaration(node) {
        if (!node.members.every(
                (member) => typeof member.initializer?.value === 'string')) {
          // Skip if it isn't a string enum.
          return;
        }
        const sortedNames =
            node.members.map((member) => member.id.name)
                .sort(
                    (a, b) => a.toLowerCase().localeCompare(b.toLowerCase()),
                );
        for (let i = 0; i < node.members.length; i++) {
          const correctName = sortedNames[i];
          const member = node.members[i];
          if (member.id.name !== correctName) {
            context.report({
              node: member,
              message: `"${member.id.name}" should be after "${correctName}".`,
            });
            break;
          }
        }
      },
    };
  },
};

/* global module */
module.exports = {
  /* eslint-disable @typescript-eslint/naming-convention */
  rules: {
    'parameter-comment-format': parameterCommentFormatRule,
    'generic-parameter-on-declaration-type': genericParameterOnDeclarationType,
    'todo-format': todoFormatRule,
    'string-enum-order': stringEnumOrder,
  },
  /* eslint-enable @typescript-eslint/naming-convention */
};