// vim: foldmethod=marker:foldmarker={{{,}}}
/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// ESlint config

// This is a matcher (usable in no-restricted-syntax) that matches either a
// test or a before/after block.
const testNameRegex =
    '/^([fx]?it|(drm|quarantined)It|(before|after)(Each|All))$/';
const testCall = `CallExpression[callee.name=${testNameRegex}]`;

const commonNoRestrictedSyntax = [
  {
    'selector':
        'MemberExpression[object.name="goog"][property.name="inherits"]',
    'message': 'Don\'t use goog.inherits.',
  },
  {
    'selector': ':not(MethodDefinition) > FunctionExpression',
    'message': 'Use arrow functions instead of "function" functions.',
  },
  {
    'selector': 'CallExpression[callee.property.name="forEach"] >' +
                ':function[params.length=1]',
    'message': 'Use for-of instead of forEach',
  },
  {
    // NOTE: prefer-spread rule covers .apply() already.
    'selector': 'CallExpression[callee.property.name=/^(bind|call)$/]',
    'message': 'Don\'t use Function bind/call.',
  },
  {
    'selector': 'MemberExpression[property.name="prototype"]',
    'message': 'Use ES6 classes not .prototype.',
  },
  {
    'selector': 'BinaryExpression[operator=/^([<>!=]=?)$/] > ' +
                'CallExpression[callee.property.name=indexOf]',
    'message': 'Use Array.includes instead of indexOf.',
  },
];

module.exports = {
  'env': {
    'browser': true,
    'es6': true,
  },
  'parserOptions': {
    'ecmaVersion': 2017,
  },
  'extends': ['eslint:recommended', 'google', 'plugin:shaka-rules/config'],
  'rules': {
    // Things the compiler already takes care of, with more precision: {{{
    'no-console': 'off',
    'no-eq-null': 'off',
    'no-eval': 'off',
    'no-undef': 'off',
    'valid-jsdoc': 'off',
    // }}}

    // Things we should probably fix, but in stages in multiple commits: {{{

    // These could catch real bugs
    'default-case': 'off',
    // TODO: Enable no-loop-func in next eslint release.  We can't use it
    // now since it doesn't allow capturing "const" variables, which is safe
    'no-loop-func': 'off',
    'no-unused-expressions': 'off',  // Conflicts with some Closure declarations
    'prefer-promise-reject-errors': 'off',

    // These could improve readability
    'complexity': 'off',
    'no-negated-condition': 'off',
    'no-shadow': 'off',
    // }}}

    // Temporary Google style overrides while we get in compliance with the
    // latest style guide {{{
    'require-jsdoc': 'off',
    // }}}

    // "Possible error" rules: {{{
    'no-async-promise-executor': 'error',
    'no-await-in-loop': 'error',
    'no-empty': ['error', {'allowEmptyCatch': true}],
    'no-misleading-character-class': 'error',
    'no-template-curly-in-string': 'error',
    'require-atomic-updates': 'error',
    // }}}

    // "Best practices" rules: {{{
    'accessor-pairs': 'error',
    'array-callback-return': 'error',
    // causes issues when implementing an interface
    'class-methods-use-this': 'off',
    'consistent-return': 'error',
    'dot-location': ['error', 'property'],
    'dot-notation': 'off',  // We use bracket notation in tests on purpose
    'eqeqeq': 'off',        // Compiler handles type checking in advance
    'guard-for-in': 'off',
    'no-alert': 'error',
    'no-caller': 'error',
    'no-div-regex': 'error',
    'no-extend-native': 'error',  // May conflict with future polyfills
    'no-extra-label': 'error',
    'no-floating-decimal': 'error',
    'no-implicit-coercion': ['error', {'allow': ['!!']}],
    'no-implied-eval': 'error',
    'no-invalid-this': 'error',
    'no-iterator': 'error',
    'no-labels': 'error',
    'no-lone-blocks': 'error',
    'no-multi-spaces': ['error', {'ignoreEOLComments': true}],
    'no-multi-str': 'error',
    'no-new': 'error',
    'no-new-func': 'error',
    'no-new-wrappers': 'error',
    'no-octal-escape': 'error',
    'no-proto': 'error',
    'no-return-assign': 'error',
    'no-return-await': 'error',
    'no-script-url': 'error',
    'no-self-compare': 'error',
    'no-sequences': 'error',
    'no-throw-literal': 'error',
    'no-unmodified-loop-condition': 'error',
    'no-unused-vars': 'off',  // Interface impls may not require all args
    'no-useless-call': 'error',
    'no-useless-catch': 'error',
    'no-useless-concat': 'error',
    'no-useless-return': 'error',
    'no-void': 'error',
    'no-warning-comments': 'off',  // TODO and FIXME are fine
    'radix': ['error', 'always'],
    'require-await': 'error',
    'wrap-iife': ['error', 'inside'],
    'yoda': ['error', 'never'],
    // }}}

    // "Variables" rules: {{{
    'no-label-var': 'error',
    'no-shadow-restricted-names': 'error',
    'no-undef-init': 'off',  // Sometimes necessary with hacky compiler casts
    'no-undefined': 'off',   // We use undefined in many places, legitimately
    // Does not know when things are executed, false positives
    'no-use-before-define': 'off',
    // }}}

    // "Stylistic Issues" rules: {{{
    'array-bracket-newline': ['error', 'consistent'],
    'block-spacing': ['error', 'always'],
    'brace-style': ['error', '1tbs', {'allowSingleLine': true}],
    'id-blacklist': ['error', 'async'],
    'lines-between-class-members': 'error',
    'max-statements-per-line': ['error', {'max': 1}],
    'new-parens': 'error',
    'no-mixed-operators': [
      'error', {
        'groups': [['&', '|', '^', '~', '<<', '>>', '>>>', '&&', '||']],
        'allowSamePrecedence': false,
      },
    ],
    'no-restricted-syntax': [
      'error',
      ...commonNoRestrictedSyntax,
    ],
    'no-whitespace-before-property': 'error',
    'nonblock-statement-body-position': ['error', 'below'],
    'operator-assignment': 'error',
    'spaced-comment': ['error', 'always', {
      // Characters which may be glued to the start of a comment block, but
      // which do not violate the rule.  The "*" is for jsdoc's "/**" syntax,
      // and the "!" is for the "/*!" of license headers which are passed
      // verbatim through the compiler.
      'markers': ['*', '!'],
    }],
    // }}}

    // "ECMAScript 6" rules: {{{
    'arrow-spacing': 'error',
    'no-useless-constructor': 'error',
    'prefer-arrow-callback': 'error',
    'prefer-const': ['error', {'ignoreReadBeforeAssign': true}],
    // }}}
  },
  'overrides': [
    {
      'rules': {
        'no-restricted-syntax': [
          'error',
          {
            'selector': 'CallExpression[callee.name="beforeAll"] ' +
                        ':matches(' +
                        'CallExpression[callee.property.name="createSpy"],' +
                        'CallExpression[callee.name="spyOn"])',
            'message': 'Create spies in beforeEach, not beforeAll.',
          },
          {
            'selector': testCall + ' > :function[params.length>0]',
            'message': 'Use async instead of "done" in tests.',
          },
          {
            'selector': testCall + ' > CallExpression',
            'message': 'Use filterDescribe instead of checkAndRun calls',
          },
          {
            'selector': 'CatchClause',
            'message': 'Use expect.toThrow or expectAsync.toBeRejected',
          },
          {
            'selector': 'CallExpression[callee.name=expect] >' +
                        'CallExpression[callee.property.name=count]' +
                        '[callee.object.property.name=calls]',
            'message': 'Use expect.toHaveBeenCalledTimes',
          },
          {
            'selector':
                'CallExpression[callee.property.name=toHaveBeenCalledTimes] >' +
                'Literal[value=0]',
            'message': 'Use expect.not.toHaveBeenCalled',
          },
          ...commonNoRestrictedSyntax,
        ],
      },
      'files': [
        'test/**/*.js',
      ],
    },
    {
      'rules': {
        'no-restricted-syntax': 'off',
      },
      'files': [
        'demo/load.js',
        'externs/**/*.js',
        'test/test/externs/*.js',
        'ui/externs/*.js',
      ],
    },
    {
      'rules': {
        'no-var': 'off',
      },
      'files': [
        // Closure requires using var in externs.
        'ui/externs/*.js',
        'externs/**/*.js',
        'test/test/externs/*.js',
        // Use var in load.js so it works in old browsers.  We'll use
        // compiled mode for the main library and the demo.
        'demo/load.js',
      ],
    },
    {
      'rules': {
        'prefer-rest-params': 'off',
      },
      'files': [
        // Test code should still use "arguments", since the alternate
        // "rest parameter" syntax won't work in uncompiled code on IE.
        'test/**/*.js',
        // These two files use "arguments" to patch over functions.  It
        // is difficult to reason about whether or not these instances
        // would always work with rest parameters, so just allow them to
        // use "arguments".
        'demo/log_section.js',
        'lib/debug/asserts.js',
      ],
    },
    {
      'files': ['externs/*', 'externs/shaka/*'],
      'rules': {
        // Disable rules on useless constructors so we can use ES6 classes in
        // externs.
        'no-useless-constructor': 'off',
      },
    },
  ],
};
