// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:linter/src/analyzer.dart';

const _desc = r"Don't implement classes that override `==`.";

const _details = r'''**DON'T** implement classes that override `==`.

The `==` operator is contractually required to be an equivalence relation;
that is, symmetrically for all objects `o1` and `o2`, `o1 == o2` and `o2 == o1`
must either both be true, or both be false.

> _NOTE_: Dart does not have true _value types_, so instead we consider a class
> that implements `==`  as a _proxy_ for identifying value types.

When using `implements`, you do not inherit the method body of `==`, making it
nearly impossible to follow the contract of `==`. Classes that override `==`
typically are usable directly in tests _without_ creating mocks or fakes as
well. For example, for a given class `Size`:

```
class Size {
  final int inBytes;
  const Size(this.inBytes);

  @override
  bool operator ==(Object other) => other is Size && other.inBytes == inBytes;

  @override
  int get hashCode => inBytes.hashCode;
}
```

**BAD**:
```
class CustomSize implements Size {
  final int inBytes;
  const CustomSize(this.inBytes);

  int get inKilobytes => inBytes ~/ 1000;
}
```

**BAD**:
```
import 'package:test/test.dart';
import 'size.dart';

class FakeSize implements Size {
  int inBytes = 0;
}

void main() {
  test('should not throw on a size >1Kb', () {
    expect(() => someFunction(new FakeSize()..inBytes = 1001), returnsNormally);
  });
}
```

**GOOD**:
```
class ExtendedSize extends Size {
  ExtendedSize(int inBytes) : super(inBytes);

  int get inKilobytes => inBytes ~/ 1000;
}
```

**GOOD**:
```
import 'package:test/test.dart';
import 'size.dart';

void main() {
  test('should not throw on a size >1Kb', () {
    expect(() => someFunction(new Size(1001)), returnsNormally);
  });
}
```

''';

class AvoidImplementingValueTypes extends LintRule
    implements NodeLintRuleWithContext {
  AvoidImplementingValueTypes()
      : super(
            name: 'avoid_implementing_value_types',
            description: _desc,
            details: _details,
            group: Group.style);

  @override
  void registerNodeProcessors(NodeLintRegistry registry,
      [LinterContext context]) {
    final visitor = new _Visitor(this);
    registry.addClassDeclaration(this, visitor);
  }
}

class _Visitor extends SimpleAstVisitor<void> {
  final LintRule rule;

  _Visitor(this.rule);

  static bool _overridesEquals(ClassElement element) =>
      element
          .lookUpConcreteMethod('==', element.library)
          ?.enclosingElement
          ?.type
          ?.isObject ==
      false;

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    if (node.implementsClause == null) {
      return;
    }
    for (TypeName interface in node.implementsClause.interfaces) {
      final element = interface.type.element;
      if (element is ClassElement && _overridesEquals(element)) {
        rule.reportLint(interface);
      }
    }
  }
}
