#!/usr/bin/env python
#
# Copyright 2012 The Closure Linter Authors. All Rights Reserved.
# 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.

"""Unit tests for the scopeutil module."""

# Allow non-Google copyright
# pylint: disable=g-bad-file-header

__author__ = ('nnaze@google.com (Nathan Naze)')


import unittest as googletest

from closure_linter import ecmametadatapass
from closure_linter import scopeutil
from closure_linter import testutil


def _FindContexts(start_token):
  """Depth first search of all contexts referenced by a token stream.

  Includes contexts' parents, which might not be directly referenced
  by any token in the stream.

  Args:
    start_token: First token in the token stream.

  Yields:
    All contexts referenced by this token stream.
  """

  seen_contexts = set()

  # For each token, yield the context if we haven't seen it before.
  for token in start_token:

    token_context = token.metadata.context
    contexts = [token_context]

    # Also grab all the context's ancestors.
    parent = token_context.parent
    while parent:
      contexts.append(parent)
      parent = parent.parent

    # Yield each of these contexts if we've not seen them.
    for context in contexts:
      if context not in seen_contexts:
        yield context

      seen_contexts.add(context)


def _FindFirstContextOfType(token, context_type):
  """Returns the first statement context."""
  for context in _FindContexts(token):
    if context.type == context_type:
      return context


class StatementTest(googletest.TestCase):

  def assertAlias(self, expected_match, script):
    start_token = testutil.TokenizeSourceAndRunEcmaPass(script)
    statement = _FindFirstContextOfType(
        start_token, ecmametadatapass.EcmaContext.STATEMENT)
    match = scopeutil.MatchAlias(statement)
    self.assertEquals(expected_match, match)

  def testSimpleAliases(self):
    self.assertAlias(
        ('foo', 'goog.foo'),
        'var foo = goog.foo;')

    self.assertAlias(
        ('foo', 'goog.foo'),
        'var foo = goog.foo')  # No semicolon

  def testAliasWithComment(self):
    self.assertAlias(
        ('Component', 'goog.ui.Component'),
        'var Component = /* comment */ goog.ui.Component;')

  def testMultilineComment(self):
    self.assertAlias(
        ('Component', 'goog.ui.Component'),
        'var Component = \n  goog.ui.Component;')

  def testNonSymbolAliasVarStatements(self):
    self.assertAlias(None, 'var foo = 3;')
    self.assertAlias(None, 'var foo = function() {};')
    self.assertAlias(None, 'for(var foo = bar;;){}')
    self.assertAlias(None, 'var foo = bar ? baz : qux;')


class ScopeBlockTest(googletest.TestCase):

  @staticmethod
  def _GetBlocks(source):
    start_token = testutil.TokenizeSourceAndRunEcmaPass(source)
    for context in _FindContexts(start_token):
      if context.type is ecmametadatapass.EcmaContext.BLOCK:
        yield context

  def assertNoBlocks(self, script):
    blocks = list(self._GetBlocks(script))
    self.assertEquals([], blocks)

  def testNotBlocks(self):
    # Ensure these are not considered blocks.
    self.assertNoBlocks('goog.scope(if{});')
    self.assertNoBlocks('goog.scope(for{});')
    self.assertNoBlocks('goog.scope(switch{});')
    self.assertNoBlocks('goog.scope(function foo{});')

  def testNonScopeBlocks(self):

    blocks = list(self._GetBlocks('goog.scope(try{});'))
    self.assertEquals(1, len(blocks))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))

    blocks = list(self._GetBlocks('goog.scope(function(a,b){});'))
    self.assertEquals(1, len(blocks))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))

    blocks = list(self._GetBlocks('goog.scope(try{} catch(){});'))
    # Two blocks: try and catch.
    self.assertEquals(2, len(blocks))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))

    blocks = list(self._GetBlocks('goog.scope(try{} catch(){} finally {});'))
    self.assertEquals(3, len(blocks))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
    self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))


class AliasTest(googletest.TestCase):

  def setUp(self):
    self.start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT)

  def testMatchAliasStatement(self):
    matches = set()
    for context in _FindContexts(self.start_token):
      match = scopeutil.MatchAlias(context)
      if match:
        matches.add(match)

    self.assertEquals(
        set([('bar', 'baz'),
             ('foo', 'this.foo_'),
             ('Component', 'goog.ui.Component'),
             ('MyClass', 'myproject.foo.MyClass'),
             ('NonClosurizedClass', 'aaa.bbb.NonClosurizedClass')]),
        matches)

  def testMatchAliasStatement_withClosurizedNamespaces(self):

    closurized_namepaces = frozenset(['goog', 'myproject'])

    matches = set()
    for context in _FindContexts(self.start_token):
      match = scopeutil.MatchAlias(context)
      if match:
        unused_alias, symbol = match
        if scopeutil.IsInClosurizedNamespace(symbol, closurized_namepaces):
          matches.add(match)

    self.assertEquals(
        set([('MyClass', 'myproject.foo.MyClass'),
             ('Component', 'goog.ui.Component')]),
        matches)

_TEST_SCRIPT = """
goog.scope(function() {
  var Component = goog.ui.Component; // scope alias
  var MyClass = myproject.foo.MyClass; // scope alias

  // Scope alias of non-Closurized namespace.
  var NonClosurizedClass = aaa.bbb.NonClosurizedClass;

  var foo = this.foo_; // non-scope object property alias
  var bar = baz; // variable alias

  var component = new Component();
});

"""

if __name__ == '__main__':
  googletest.main()
