/**
 * @license
 * Copyright Google LLC
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {Replacement, RuleFailure, WalkContext} from 'tslint/lib';
import {TypedRule} from 'tslint/lib/rules';
import ts from 'typescript';

const FAILURE_MESSAGE =
  'Missing override modifier. Members implemented as part of ' +
  'abstract classes must explicitly set the "override" modifier. ' +
  'More details: https://github.com/microsoft/TypeScript/issues/44457#issuecomment-856202843.';

/**
 * Rule which enforces that class members implementing abstract members
 * from base classes explicitly specify the `override` modifier.
 *
 * This ensures we follow the best-practice of applying `override` for abstract-implemented
 * members so that TypeScript creates diagnostics in both scenarios where either the abstract
 * class member is removed, or renamed.
 *
 * More details can be found here: https://github.com/microsoft/TypeScript/issues/44457.
 */
export class Rule extends TypedRule {
  override applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
    return this.applyWithFunction(sourceFile, (ctx) => visitNode(sourceFile, ctx, program));
  }
}

/**
 * For a TypeScript AST node and each of its child nodes, check whether the node is a class
 * element which implements an abstract member but does not have the `override` keyword.
 */
function visitNode(node: ts.Node, ctx: WalkContext, program: ts.Program) {
  // If a class element implements an abstract member but does not have the
  // `override` keyword, create a lint failure.
  if (
    ts.isClassElement(node) &&
    !hasOverrideModifier(node) &&
    matchesParentAbstractElement(node, program)
  ) {
    ctx.addFailureAtNode(
      node,
      FAILURE_MESSAGE,
      Replacement.appendText(node.getStart(), `override `),
    );
  }

  ts.forEachChild(node, (n) => visitNode(n, ctx, program));
}

/**
 * Checks if the specified class element matches a parent abstract class element. i.e.
 * whether the specified member "implements" an abstract member from a base class.
 */
function matchesParentAbstractElement(node: ts.ClassElement, program: ts.Program): boolean {
  const containingClass = node.parent as ts.ClassDeclaration;

  // If the property we check does not have a property name, we cannot look for similarly-named
  // members in parent classes and therefore return early.
  if (node.name === undefined) {
    return false;
  }

  const propertyName = getPropertyNameText(node.name);
  const typeChecker = program.getTypeChecker();

  // If the property we check does not have a statically-analyzable property name,
  // we cannot look for similarly-named members in parent classes and return early.
  if (propertyName === null) {
    return false;
  }

  return checkClassForInheritedMatchingAbstractMember(containingClass, typeChecker, propertyName);
}

/** Checks if the given class inherits an abstract member with the specified name. */
function checkClassForInheritedMatchingAbstractMember(
  clazz: ts.ClassDeclaration,
  typeChecker: ts.TypeChecker,
  searchMemberName: string,
): boolean {
  const baseClass = getBaseClass(clazz, typeChecker);

  // If the class is not `abstract`, then all parent abstract methods would need to
  // be implemented, and there is never an abstract member within the class.
  if (baseClass === null || !hasAbstractModifier(baseClass)) {
    return false;
  }

  const matchingMember = baseClass.members.find(
    (m) => m.name !== undefined && getPropertyNameText(m.name) === searchMemberName,
  );

  if (matchingMember !== undefined) {
    return hasAbstractModifier(matchingMember);
  }

  return checkClassForInheritedMatchingAbstractMember(baseClass, typeChecker, searchMemberName);
}

/** Gets the base class for the given class declaration. */
function getBaseClass(
  node: ts.ClassDeclaration,
  typeChecker: ts.TypeChecker,
): ts.ClassDeclaration | null {
  const baseTypes = getExtendsHeritageExpressions(node);

  if (baseTypes.length > 1) {
    throw Error('Class unexpectedly extends from multiple types.');
  }

  const baseClass = typeChecker.getTypeAtLocation(baseTypes[0]).getSymbol();
  const baseClassDecl = baseClass?.valueDeclaration ?? baseClass?.declarations?.[0];

  if (baseClassDecl !== undefined && ts.isClassDeclaration(baseClassDecl)) {
    return baseClassDecl;
  }

  return null;
}

/** Gets the `extends` base type expressions of the specified class. */
function getExtendsHeritageExpressions(
  classDecl: ts.ClassDeclaration,
): ts.ExpressionWithTypeArguments[] {
  if (classDecl.heritageClauses === undefined) {
    return [];
  }
  const result: ts.ExpressionWithTypeArguments[] = [];
  for (const clause of classDecl.heritageClauses) {
    if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
      result.push(...clause.types);
    }
  }
  return result;
}

/** Gets whether the specified node has the `abstract` modifier applied. */
function hasAbstractModifier(node: ts.Node): boolean {
  if (!ts.canHaveModifiers(node)) {
    return false;
  }
  return !!ts
    .getModifiers(node)
    ?.some((s: ts.Modifier) => s.kind === ts.SyntaxKind.AbstractKeyword);
}

/** Gets whether the specified node has the `override` modifier applied. */
function hasOverrideModifier(node: ts.Node): boolean {
  return !!(node as any).modifiers?.some(
    (s: ts.Modifier) => s.kind === ts.SyntaxKind.OverrideKeyword,
  );
}

/** Gets the property name text of the specified property name. */
function getPropertyNameText(name: ts.PropertyName): string | null {
  if (ts.isComputedPropertyName(name)) {
    return null;
  }
  return name.text;
}
