/**
 * Copyright 2014 Shape Security, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const { reduce, MonoidalReducer } = require('shift-reducer');
const { isStrictModeReservedWord } = require('./utils');
const { ErrorMessages } = require('./errors');

const { EarlyErrorState, EarlyError } = require('./early-error-state');

function isStrictFunctionBody({ directives }) {
  return directives.some(directive => directive.rawValue === 'use strict');
}

function isLabelledFunction(node) {
  return node.type === 'LabeledStatement' &&
    (node.body.type === 'FunctionDeclaration' || isLabelledFunction(node.body));
}

function isIterationStatement(node) {
  switch (node.type) {
    case 'LabeledStatement':
      return isIterationStatement(node.body);
    case 'DoWhileStatement':
    case 'ForInStatement':
    case 'ForOfStatement':
    case 'ForStatement':
    case 'WhileStatement':
      return true;
  }
  return false;
}

function isSpecialMethod(methodDefinition) {
  if (methodDefinition.name.type !== 'StaticPropertyName' || methodDefinition.name.value !== 'constructor') {
    return false;
  }
  switch (methodDefinition.type) {
    case 'Getter':
    case 'Setter':
      return true;
    case 'Method':
      return methodDefinition.isGenerator || methodDefinition.isAsync;
  }
  /* istanbul ignore next */
  throw new Error('not reached');
}


function enforceDuplicateConstructorMethods(node, s) {
  let ctors = node.elements.filter(e =>
    !e.isStatic &&
    e.method.type === 'Method' &&
    !e.method.isGenerator &&
    e.method.name.type === 'StaticPropertyName' &&
    e.method.name.value === 'constructor'
  );
  if (ctors.length > 1) {
    ctors.slice(1).forEach(ctor => {
      s = s.addError(new EarlyError(ctor, 'Duplicate constructor method in class'));
    });
  }
  return s;
}

const SUPERCALL_ERROR = node => new EarlyError(node, ErrorMessages.ILLEGAL_SUPER_CALL);
const SUPERPROPERTY_ERROR = node => new EarlyError(node, 'Member access on super must be in a method');
const DUPLICATE_BINDING = node => new EarlyError(node, `Duplicate binding ${JSON.stringify(node.name)}`);
const FREE_CONTINUE = node => new EarlyError(node, 'Continue statement must be nested within an iteration statement');
const UNBOUND_CONTINUE = node => new EarlyError(node, `Continue statement must be nested within an iteration statement with label ${JSON.stringify(node.label)}`);
const FREE_BREAK = node => new EarlyError(node, 'Break statement must be nested within an iteration statement or a switch statement');
const UNBOUND_BREAK = node => new EarlyError(node, `Break statement must be nested within a statement with label ${JSON.stringify(node.label)}`);

class EarlyErrorChecker extends MonoidalReducer {
  constructor() {
    super(EarlyErrorState);
  }

  reduceAssignmentExpression() {
    return super.reduceAssignmentExpression(...arguments).clearBoundNames();
  }

  reduceAssignmentTargetIdentifier(node) {
    let s = this.identity;
    if (node.name === 'eval' || node.name === 'arguments' || isStrictModeReservedWord(node.name)) {
      s = s.addStrictError(new EarlyError(node, `The identifier ${JSON.stringify(node.name)} must not be in binding position in strict mode`));
    }
    return s;
  }

  reduceArrowExpression(node, { params, body }) {
    let isSimpleParameterList = node.params.rest == null && node.params.items.every(i => i.type === 'BindingIdentifier');
    params = params.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    if (node.body.type === 'FunctionBody') {
      body = body.enforceConflictingLexicallyDeclaredNames(params.lexicallyDeclaredNames, DUPLICATE_BINDING);
      if (isStrictFunctionBody(node.body)) {
        params = params.enforceStrictErrors();
        body = body.enforceStrictErrors();
      }
    }
    params.yieldExpressions.forEach(n => {
      params = params.addError(new EarlyError(n, 'Arrow parameters must not contain yield expressions'));
    });
    params.awaitExpressions.forEach(n => {
      params = params.addError(new EarlyError(n, 'Arrow parameters must not contain await expressions'));
    });
    let s = super.reduceArrowExpression(node, { params, body });
    if (!isSimpleParameterList && node.body.type === 'FunctionBody' && isStrictFunctionBody(node.body)) {
      s = s.addError(new EarlyError(node, 'Functions with non-simple parameter lists may not contain a "use strict" directive'));
    }
    s = s.clearYieldExpressions();
    s = s.clearAwaitExpressions();
    s = s.observeVarBoundary();
    return s;
  }

  reduceAwaitExpression(node, { expression }) {
    return expression.observeAwaitExpression(node);
  }

  reduceBindingIdentifier(node) {
    let s = this.identity;
    if (node.name === 'eval' || node.name === 'arguments' || isStrictModeReservedWord(node.name)) {
      s = s.addStrictError(new EarlyError(node, `The identifier ${JSON.stringify(node.name)} must not be in binding position in strict mode`));
    }
    s = s.bindName(node.name, node);
    return s;
  }

  reduceBlock() {
    let s = super.reduceBlock(...arguments);
    s = s.functionDeclarationNamesAreLexical();
    s = s.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    s = s.enforceConflictingLexicallyDeclaredNames(s.varDeclaredNames, DUPLICATE_BINDING);
    s = s.observeLexicalBoundary();
    return s;
  }

  reduceBreakStatement(node) {
    let s = super.reduceBreakStatement(...arguments);
    s = node.label == null
      ? s.addFreeBreakStatement(node)
      : s.addFreeLabeledBreakStatement(node);
    return s;
  }

  reduceCallExpression(node) {
    let s = super.reduceCallExpression(...arguments);
    if (node.callee.type === 'Super') {
      s = s.observeSuperCallExpression(node);
    }
    return s;
  }

  reduceCatchClause(node, { binding, body }) {
    if (binding != null) {
      binding = binding.observeLexicalDeclaration();
      binding = binding.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
      binding = binding.enforceConflictingLexicallyDeclaredNames(body.previousLexicallyDeclaredNames, DUPLICATE_BINDING);
    }
    let s = super.reduceCatchClause(node, { binding, body });
    s = s.observeLexicalBoundary();
    return s;
  }

  reduceClassDeclaration(node, { name, super: _super, elements }) {
    let s = name.enforceStrictErrors();
    let sElements = this.append(...elements);
    sElements = sElements.enforceStrictErrors();
    if (node.super != null) {
      _super = _super.enforceStrictErrors();
      s = this.append(s, _super);
      sElements = sElements.clearSuperCallExpressionsInConstructorMethod();
    }
    s = this.append(s, sElements);
    s = enforceDuplicateConstructorMethods(node, s);
    s = s.observeLexicalDeclaration();
    return s;
  }

  reduceClassElement(node) {
    let s = super.reduceClassElement(...arguments);
    if (!node.isStatic && isSpecialMethod(node.method)) {
      s = s.addError(new EarlyError(node, ErrorMessages.ILLEGAL_CONSTRUCTORS));
    }
    if (node.isStatic && node.method.name.type === 'StaticPropertyName' && node.method.name.value === 'prototype') {
      s = s.addError(new EarlyError(node, 'Static class methods cannot be named "prototype"'));
    }
    return s;
  }

  reduceClassExpression(node, { name, super: _super, elements }) {
    let s = node.name == null ? this.identity : name.enforceStrictErrors();
    let sElements = this.append(...elements);
    sElements = sElements.enforceStrictErrors();
    if (node.super != null) {
      _super = _super.enforceStrictErrors();
      s = this.append(s, _super);
      sElements = sElements.clearSuperCallExpressionsInConstructorMethod();
    }
    s = this.append(s, sElements);
    s = enforceDuplicateConstructorMethods(node, s);
    s = s.clearBoundNames();
    return s;
  }

  reduceCompoundAssignmentExpression() {
    return super.reduceCompoundAssignmentExpression(...arguments).clearBoundNames();
  }

  reduceComputedMemberExpression(node) {
    let s = super.reduceComputedMemberExpression(...arguments);
    if (node.object.type === 'Super') {
      s = s.observeSuperPropertyExpression(node);
    }
    return s;
  }

  reduceContinueStatement(node) {
    let s = super.reduceContinueStatement(...arguments);
    s = node.label == null
      ? s.addFreeContinueStatement(node)
      : s.addFreeLabeledContinueStatement(node);
    return s;
  }

  reduceDoWhileStatement(node) {
    let s = super.reduceDoWhileStatement(...arguments);
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a do-while statement must not be a labeled function declaration'));
    }
    s = s.clearFreeContinueStatements();
    s = s.clearFreeBreakStatements();
    return s;
  }

  reduceExport() {
    let s = super.reduceExport(...arguments);
    s = s.functionDeclarationNamesAreLexical();
    s = s.exportDeclaredNames();
    return s;
  }

  reduceExportFrom() {
    let s = super.reduceExportFrom(...arguments);
    s = s.clearExportedBindings();
    return s;
  }

  reduceExportFromSpecifier(node) {
    let s = super.reduceExportFromSpecifier(...arguments);
    s = s.exportName(node.exportedName || node.name, node);
    s = s.exportBinding(node.name, node);
    return s;
  }

  reduceExportLocalSpecifier(node) {
    let s = super.reduceExportLocalSpecifier(...arguments);
    s = s.exportName(node.exportedName || node.name.name, node);
    s = s.exportBinding(node.name.name, node);
    return s;
  }

  reduceExportDefault(node) {
    let s = super.reduceExportDefault(...arguments);
    s = s.functionDeclarationNamesAreLexical();
    s = s.exportName('default', node);
    return s;
  }

  reduceFormalParameters() {
    let s = super.reduceFormalParameters(...arguments);
    s = s.observeLexicalDeclaration();
    return s;
  }

  reduceForStatement(node, { init, test, update, body }) {
    if (init != null) {
      init = init.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
      init = init.enforceConflictingLexicallyDeclaredNames(body.varDeclaredNames, DUPLICATE_BINDING);
    }
    let s = super.reduceForStatement(node, { init, test, update, body });
    if (node.init != null && node.init.type === 'VariableDeclaration' && node.init.kind === 'const') {
      node.init.declarators.forEach(declarator => {
        if (declarator.init == null) {
          s = s.addError(new EarlyError(declarator, 'Constant lexical declarations must have an initialiser'));
        }
      });
    }
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a for statement must not be a labeled function declaration'));
    }
    s = s.clearFreeContinueStatements();
    s = s.clearFreeBreakStatements();
    s = s.observeLexicalBoundary();
    return s;
  }

  reduceForInStatement(node, { left, right, body }) {
    left = left.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    left = left.enforceConflictingLexicallyDeclaredNames(body.varDeclaredNames, DUPLICATE_BINDING);
    let s = super.reduceForInStatement(node, { left, right, body });
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a for-in statement must not be a labeled function declaration'));
    }
    s = s.clearFreeContinueStatements();
    s = s.clearFreeBreakStatements();
    s = s.observeLexicalBoundary();
    return s;
  }

  reduceForOfStatement(node, { left, right, body }) {
    left = left.recordForOfVars();
    left = left.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    left = left.enforceConflictingLexicallyDeclaredNames(body.varDeclaredNames, DUPLICATE_BINDING);
    let s = super.reduceForOfStatement(node, { left, right, body });
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a for-of statement must not be a labeled function declaration'));
    }
    s = s.clearFreeContinueStatements();
    s = s.clearFreeBreakStatements();
    s = s.observeLexicalBoundary();
    return s;
  }

  reduceForAwaitStatement(node, { left, right, body }) {
    left = left.recordForOfVars();
    left = left.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    left = left.enforceConflictingLexicallyDeclaredNames(body.varDeclaredNames, DUPLICATE_BINDING);
    let s = super.reduceForOfStatement(node, { left, right, body });
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a for-await statement must not be a labeled function declaration'));
    }
    s = s.clearFreeContinueStatements();
    s = s.clearFreeBreakStatements();
    s = s.observeLexicalBoundary();
    return s;
  }

  reduceFunctionBody(node) {
    let s = super.reduceFunctionBody(...arguments);
    s = s.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    s = s.enforceConflictingLexicallyDeclaredNames(s.varDeclaredNames, DUPLICATE_BINDING);
    s = s.enforceFreeContinueStatementErrors(FREE_CONTINUE);
    s = s.enforceFreeLabeledContinueStatementErrors(UNBOUND_CONTINUE);
    s = s.enforceFreeBreakStatementErrors(FREE_BREAK);
    s = s.enforceFreeLabeledBreakStatementErrors(UNBOUND_BREAK);
    s = s.clearUsedLabelNames();
    s = s.clearYieldExpressions();
    s = s.clearAwaitExpressions();
    if (isStrictFunctionBody(node)) {
      s = s.enforceStrictErrors();
    }
    return s;
  }

  reduceFunctionDeclaration(node, { name, params, body }) {
    let isSimpleParameterList = node.params.rest == null && node.params.items.every(i => i.type === 'BindingIdentifier');
    let addError = !isSimpleParameterList || node.isGenerator ? 'addError' : 'addStrictError';
    params.lexicallyDeclaredNames.forEachEntry(nodes => {
      if (nodes.length > 1) {
        nodes.slice(1).forEach(dupeNode => {
          params = params[addError](DUPLICATE_BINDING(dupeNode));
        });
      }
    });
    body = body.enforceConflictingLexicallyDeclaredNames(params.lexicallyDeclaredNames, DUPLICATE_BINDING);
    body = body.enforceSuperCallExpressions(SUPERCALL_ERROR);
    body = body.enforceSuperPropertyExpressions(SUPERPROPERTY_ERROR);
    params = params.enforceSuperCallExpressions(SUPERCALL_ERROR);
    params = params.enforceSuperPropertyExpressions(SUPERPROPERTY_ERROR);
    if (node.isGenerator) {
      params.yieldExpressions.forEach(n => {
        params = params.addError(new EarlyError(n, 'Generator parameters must not contain yield expressions'));
      });
    }
    if (node.isAsync) {
      params.awaitExpressions.forEach(n => {
        params = params.addError(new EarlyError(n, 'Async function parameters must not contain await expressions'));
      });
    }
    params = params.clearNewTargetExpressions();
    body = body.clearNewTargetExpressions();
    if (isStrictFunctionBody(node.body)) {
      params = params.enforceStrictErrors();
      body = body.enforceStrictErrors();
    }
    let s = super.reduceFunctionDeclaration(node, { name, params, body });
    if (!isSimpleParameterList && isStrictFunctionBody(node.body)) {
      s = s.addError(new EarlyError(node, 'Functions with non-simple parameter lists may not contain a "use strict" directive'));
    }
    s = s.clearYieldExpressions();
    s = s.clearAwaitExpressions();
    s = s.observeFunctionDeclaration();
    return s;
  }

  reduceFunctionExpression(node, { name, params, body }) {
    let isSimpleParameterList = node.params.rest == null && node.params.items.every(i => i.type === 'BindingIdentifier');
    let addError = !isSimpleParameterList || node.isGenerator ? 'addError' : 'addStrictError';
    params.lexicallyDeclaredNames.forEachEntry((nodes, bindingName) => {
      if (nodes.length > 1) {
        nodes.slice(1).forEach(dupeNode => {
          params = params[addError](new EarlyError(dupeNode, `Duplicate binding ${JSON.stringify(bindingName)}`));
        });
      }
    });
    body = body.enforceConflictingLexicallyDeclaredNames(params.lexicallyDeclaredNames, DUPLICATE_BINDING);
    body = body.enforceSuperCallExpressions(SUPERCALL_ERROR);
    body = body.enforceSuperPropertyExpressions(SUPERPROPERTY_ERROR);
    params = params.enforceSuperCallExpressions(SUPERCALL_ERROR);
    params = params.enforceSuperPropertyExpressions(SUPERPROPERTY_ERROR);
    if (node.isGenerator) {
      params.yieldExpressions.forEach(n => {
        params = params.addError(new EarlyError(n, 'Generator parameters must not contain yield expressions'));
      });
    }
    if (node.isAsync) {
      params.awaitExpressions.forEach(n => {
        params = params.addError(new EarlyError(n, 'Async function parameters must not contain await expressions'));
      });
    }
    params = params.clearNewTargetExpressions();
    body = body.clearNewTargetExpressions();
    if (isStrictFunctionBody(node.body)) {
      params = params.enforceStrictErrors();
      body = body.enforceStrictErrors();
    }
    let s = super.reduceFunctionExpression(node, { name, params, body });
    if (!isSimpleParameterList && isStrictFunctionBody(node.body)) {
      s = s.addError(new EarlyError(node, 'Functions with non-simple parameter lists may not contain a "use strict" directive'));
    }
    s = s.clearBoundNames();
    s = s.clearYieldExpressions();
    s = s.clearAwaitExpressions();
    s = s.observeVarBoundary();
    return s;
  }

  reduceGetter(node, { name, body }) {
    body = body.enforceSuperCallExpressions(SUPERCALL_ERROR);
    body = body.clearSuperPropertyExpressions();
    body = body.clearNewTargetExpressions();
    if (isStrictFunctionBody(node.body)) {
      body = body.enforceStrictErrors();
    }
    let s = super.reduceGetter(node, { name, body });
    s = s.observeVarBoundary();
    return s;
  }

  reduceIdentifierExpression(node) {
    let s = this.identity;
    if (isStrictModeReservedWord(node.name)) {
      s = s.addStrictError(new EarlyError(node, `The identifier ${JSON.stringify(node.name)} must not be in expression position in strict mode`));
    }
    return s;
  }

  reduceIfStatement(node, { test, consequent, alternate }) {
    if (isLabelledFunction(node.consequent)) {
      consequent = consequent.addError(new EarlyError(node.consequent, 'The consequent of an if statement must not be a labeled function declaration'));
    }
    if (node.alternate != null && isLabelledFunction(node.alternate)) {
      alternate = alternate.addError(new EarlyError(node.alternate, 'The alternate of an if statement must not be a labeled function declaration'));
    }
    if (node.consequent.type === 'FunctionDeclaration') {
      consequent = consequent.addStrictError(new EarlyError(node.consequent, 'FunctionDeclarations in IfStatements are disallowed in strict mode'));
      consequent = consequent.observeLexicalBoundary();
    }
    if (node.alternate != null && node.alternate.type === 'FunctionDeclaration') {
      alternate = alternate.addStrictError(new EarlyError(node.alternate, 'FunctionDeclarations in IfStatements are disallowed in strict mode'));
      alternate = alternate.observeLexicalBoundary();
    }
    return super.reduceIfStatement(node, { test, consequent, alternate });
  }

  reduceImport() {
    let s = super.reduceImport(...arguments);
    s = s.observeLexicalDeclaration();
    return s;
  }

  reduceImportNamespace() {
    let s = super.reduceImportNamespace(...arguments);
    s = s.observeLexicalDeclaration();
    return s;
  }

  reduceLabeledStatement(node) {
    let s = super.reduceLabeledStatement(...arguments);
    if (node.label === 'yield' || isStrictModeReservedWord(node.label)) {
      s = s.addStrictError(new EarlyError(node, `The identifier ${JSON.stringify(node.label)} must not be in label position in strict mode`));
    }
    if (s.usedLabelNames.indexOf(node.label) >= 0) {
      s = s.addError(new EarlyError(node, `Label ${JSON.stringify(node.label)} has already been declared`));
    }
    if (node.body.type === 'FunctionDeclaration') {
      s = s.addStrictError(new EarlyError(node, 'Labeled FunctionDeclarations are disallowed in strict mode'));
    }
    s = isIterationStatement(node.body)
      ? s.observeIterationLabel(node.label)
      : s.observeNonIterationLabel(node.label);
    return s;
  }

  reduceLiteralRegExpExpression() {
    let s = this.identity;
    // NOTE: the RegExp pattern acceptor is disabled until we have more confidence in its correctness (more tests)
    // if (!PatternAcceptor.test(node.pattern, node.flags.indexOf("u") >= 0)) {
    //  s = s.addError(new EarlyError(node, "Invalid regular expression pattern"));
    // }
    return s;
  }

  reduceMethod(node, { name, params, body }) {
    let isSimpleParameterList = node.params.rest == null && node.params.items.every(i => i.type === 'BindingIdentifier');
    params = params.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    body = body.enforceConflictingLexicallyDeclaredNames(params.lexicallyDeclaredNames, DUPLICATE_BINDING);
    if (node.name.type === 'StaticPropertyName' && node.name.value === 'constructor') {
      body = body.observeConstructorMethod();
      params = params.observeConstructorMethod();
    } else {
      body = body.enforceSuperCallExpressions(SUPERCALL_ERROR);
      params = params.enforceSuperCallExpressions(SUPERCALL_ERROR);
    }
    if (node.isGenerator) {
      params.yieldExpressions.forEach(n => {
        params = params.addError(new EarlyError(n, 'Generator parameters must not contain yield expressions'));
      });
    }
    if (node.isAsync) {
      params.awaitExpressions.forEach(n => {
        params = params.addError(new EarlyError(n, 'Async function parameters must not contain await expressions'));
      });
    }
    body = body.clearSuperPropertyExpressions();
    params = params.clearSuperPropertyExpressions();
    params = params.clearNewTargetExpressions();
    body = body.clearNewTargetExpressions();
    if (isStrictFunctionBody(node.body)) {
      params = params.enforceStrictErrors();
      body = body.enforceStrictErrors();
    }
    let s = super.reduceMethod(node, { name, params, body });
    if (!isSimpleParameterList && isStrictFunctionBody(node.body)) {
      s = s.addError(new EarlyError(node, 'Functions with non-simple parameter lists may not contain a "use strict" directive'));
    }
    s = s.clearYieldExpressions();
    s = s.clearAwaitExpressions();
    s = s.observeVarBoundary();
    return s;
  }

  reduceModule() {
    let s = super.reduceModule(...arguments);
    s = s.functionDeclarationNamesAreLexical();
    s = s.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    s = s.enforceConflictingLexicallyDeclaredNames(s.varDeclaredNames, DUPLICATE_BINDING);
    s.exportedNames.forEachEntry((nodes, bindingName) => {
      if (nodes.length > 1) {
        nodes.slice(1).forEach(dupeNode => {
          s = s.addError(new EarlyError(dupeNode, `Duplicate export ${JSON.stringify(bindingName)}`));
        });
      }
    });
    s.exportedBindings.forEachEntry((nodes, bindingName) => {
      if (!s.lexicallyDeclaredNames.has(bindingName) && !s.varDeclaredNames.has(bindingName)) {
        nodes.forEach(undeclaredNode => {
          s = s.addError(new EarlyError(undeclaredNode, `Exported binding ${JSON.stringify(bindingName)} is not declared`));
        });
      }
    });
    s.newTargetExpressions.forEach(node => {
      s = s.addError(new EarlyError(node, 'new.target must be within function (but not arrow expression) code'));
    });
    s = s.enforceFreeContinueStatementErrors(FREE_CONTINUE);
    s = s.enforceFreeLabeledContinueStatementErrors(UNBOUND_CONTINUE);
    s = s.enforceFreeBreakStatementErrors(FREE_BREAK);
    s = s.enforceFreeLabeledBreakStatementErrors(UNBOUND_BREAK);
    s = s.enforceSuperCallExpressions(SUPERCALL_ERROR);
    s = s.enforceSuperPropertyExpressions(SUPERPROPERTY_ERROR);
    s = s.enforceStrictErrors();
    return s;
  }

  reduceNewTargetExpression(node) {
    return this.identity.observeNewTargetExpression(node);
  }

  reduceObjectExpression(node) {
    let s = super.reduceObjectExpression(...arguments);
    s = s.enforceSuperCallExpressionsInConstructorMethod(SUPERCALL_ERROR);
    let protos = node.properties.filter(p => p.type === 'DataProperty' && p.name.type === 'StaticPropertyName' && p.name.value === '__proto__');
    protos.slice(1).forEach(n => {
      s = s.addError(new EarlyError(n, 'Duplicate __proto__ property in object literal not allowed'));
    });
    return s;
  }

  reduceUpdateExpression() {
    let s = super.reduceUpdateExpression(...arguments);
    s = s.clearBoundNames();
    return s;
  }

  reduceUnaryExpression(node) {
    let s = super.reduceUnaryExpression(...arguments);
    if (node.operator === 'delete' && node.operand.type === 'IdentifierExpression') {
      s = s.addStrictError(new EarlyError(node, 'Identifier expressions must not be deleted in strict mode'));
    }
    return s;
  }

  reduceScript(node) {
    let s = super.reduceScript(...arguments);
    s = s.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    s = s.enforceConflictingLexicallyDeclaredNames(s.varDeclaredNames, DUPLICATE_BINDING);
    s.newTargetExpressions.forEach(n => {
      s = s.addError(new EarlyError(n, 'new.target must be within function (but not arrow expression) code'));
    });
    s = s.enforceFreeContinueStatementErrors(FREE_CONTINUE);
    s = s.enforceFreeLabeledContinueStatementErrors(UNBOUND_CONTINUE);
    s = s.enforceFreeBreakStatementErrors(FREE_BREAK);
    s = s.enforceFreeLabeledBreakStatementErrors(UNBOUND_BREAK);
    s = s.enforceSuperCallExpressions(SUPERCALL_ERROR);
    s = s.enforceSuperPropertyExpressions(SUPERPROPERTY_ERROR);
    if (isStrictFunctionBody(node)) {
      s = s.enforceStrictErrors();
    }
    return s;
  }

  reduceSetter(node, { name, param, body }) {
    let isSimpleParameterList = node.param.type === 'BindingIdentifier';
    param = param.observeLexicalDeclaration();
    param = param.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    body = body.enforceConflictingLexicallyDeclaredNames(param.lexicallyDeclaredNames, DUPLICATE_BINDING);
    param = param.enforceSuperCallExpressions(SUPERCALL_ERROR);
    body = body.enforceSuperCallExpressions(SUPERCALL_ERROR);
    param = param.clearSuperPropertyExpressions();
    body = body.clearSuperPropertyExpressions();
    param = param.clearNewTargetExpressions();
    body = body.clearNewTargetExpressions();
    if (isStrictFunctionBody(node.body)) {
      param = param.enforceStrictErrors();
      body = body.enforceStrictErrors();
    }
    let s = super.reduceSetter(node, { name, param, body });
    if (!isSimpleParameterList && isStrictFunctionBody(node.body)) {
      s = s.addError(new EarlyError(node, 'Functions with non-simple parameter lists may not contain a "use strict" directive'));
    }
    s = s.observeVarBoundary();
    return s;
  }

  reduceStaticMemberExpression(node) {
    let s = super.reduceStaticMemberExpression(...arguments);
    if (node.object.type === 'Super') {
      s = s.observeSuperPropertyExpression(node);
    }
    return s;
  }

  reduceSwitchStatement(node, { discriminant, cases }) {
    let sCases = this.append(...cases);
    sCases = sCases.functionDeclarationNamesAreLexical();
    sCases = sCases.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    sCases = sCases.enforceConflictingLexicallyDeclaredNames(sCases.varDeclaredNames, DUPLICATE_BINDING);
    sCases = sCases.observeLexicalBoundary();
    let s = this.append(discriminant, sCases);
    s = s.clearFreeBreakStatements();
    return s;
  }

  reduceSwitchStatementWithDefault(node, { discriminant, preDefaultCases, defaultCase, postDefaultCases }) {
    let sCases = this.append(defaultCase, ...preDefaultCases, ...postDefaultCases);
    sCases = sCases.functionDeclarationNamesAreLexical();
    sCases = sCases.enforceDuplicateLexicallyDeclaredNames(DUPLICATE_BINDING);
    sCases = sCases.enforceConflictingLexicallyDeclaredNames(sCases.varDeclaredNames, DUPLICATE_BINDING);
    sCases = sCases.observeLexicalBoundary();
    let s = this.append(discriminant, sCases);
    s = s.clearFreeBreakStatements();
    return s;
  }

  reduceVariableDeclaration(node) {
    let s = super.reduceVariableDeclaration(...arguments);
    switch (node.kind) {
      case 'const':
      case 'let': {
        s = s.observeLexicalDeclaration();
        if (s.lexicallyDeclaredNames.has('let')) {
          s.lexicallyDeclaredNames.get('let').forEach(n => {
            s = s.addError(new EarlyError(n, 'Lexical declarations must not have a binding named "let"'));
          });
        }
        break;
      }
      case 'var':
        s = s.observeVarDeclaration();
        break;
    }
    return s;
  }

  reduceVariableDeclarationStatement(node) {
    let s = super.reduceVariableDeclarationStatement(...arguments);
    if (node.declaration.kind === 'const') {
      node.declaration.declarators.forEach(declarator => {
        if (declarator.init == null) {
          s = s.addError(new EarlyError(declarator, 'Constant lexical declarations must have an initialiser'));
        }
      });
    }
    return s;
  }

  reduceWhileStatement(node) {
    let s = super.reduceWhileStatement(...arguments);
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a while statement must not be a labeled function declaration'));
    }
    s = s.clearFreeContinueStatements().clearFreeBreakStatements();
    return s;
  }

  reduceWithStatement(node) {
    let s = super.reduceWithStatement(...arguments);
    if (isLabelledFunction(node.body)) {
      s = s.addError(new EarlyError(node.body, 'The body of a with statement must not be a labeled function declaration'));
    }
    s = s.addStrictError(new EarlyError(node, 'Strict mode code must not include a with statement'));
    return s;
  }

  reduceYieldExpression(node) {
    let s = super.reduceYieldExpression(...arguments);
    s = s.observeYieldExpression(node);
    return s;
  }

  reduceYieldGeneratorExpression(node) {
    let s = super.reduceYieldGeneratorExpression(...arguments);
    s = s.observeYieldExpression(node);
    return s;
  }


  static check(node) {
    return reduce(new EarlyErrorChecker, node).errors;
  }
}

module.exports = { EarlyErrorChecker };
