Repository: neogeny/TatSu

Issue:
Ignorecase needs uppercase keywords when reserving
Reserving keywords and ignoring case does not work as expected:

```
@@ignorecase :: True
@@keyword :: if

@name
rule = /\w+/  if_exp $ ;

if_exp = 'if' /\d/ ;
```
**The 'if' is parsed as the rule while it should error.**

This is because in [contexts.py](https://github.com/neogeny/TatSu/blob/master/tatsu/contexts.py), you should apply either `upper` (or `lower`) to both name and keywords:
```
def _check_name(self, name=None):
    if name is None:
        name = str(self.last_node)
    if self.ignorecase or self._tokenizer.ignorecase:
        name = name.upper()
    if name in self.keywords:
        raise FailedKeywordSemantics('"%s" is a reserved word' % name)
```



Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
index 75a1593..280bbe7 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -10,7 +10,7 @@ extension-pkg-whitelist=
 
 # Add files or directories to the blacklist. They should be base names, not
 # paths.
-ignore=CVS,bootstrap.py,model.py
+ignore=CVS,bootstrap.py,model.py,tmp
 
 # Add files or directories matching the regex patterns to the blacklist. The
 # regex matches against base names, not paths.
diff --git a/README.md b/README.md
index e16689f..a3df3b6 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 [![license](https://img.shields.io/badge/license-BSD-blue.svg)](https://raw.githubusercontent.com/neogeny/tatsu/master/LICENSE.txt) [![pyversions](https://img.shields.io/pypi/pyversions/tatsu.svg)](https://pypi.python.org/pypi/tatsu) [![fury](https://badge.fury.io/py/TatSu.svg)](https://badge.fury.io/py/TatSu) [![circleci](https://circleci.com/gh/neogeny/TatSu.svg?style=shield)](https://circleci.com/gh/neogeny/TatSu) [![docs](https://readthedocs.org/projects/tatsu/badge/?version=stable)](http://tatsu.readthedocs.io/en/stable/)
 
 > *At least for the people who send me mail about a new language that they're designing, the general advice is: do it to learn about how to write a compiler. Don't have any expectations that anyone will use it, unless you hook up with some sort of organization in a position to push it hard. It's a lottery, and some can buy a lot of the tickets. There are plenty of beautiful languages (more beautiful than C) that didn't catch on. But someone does win the lottery, and doing a language at least teaches you something.*
-> 
+>
 > [Dennis Ritchie](http://en.wikipedia.org/wiki/Dennis_Ritchie) (1941-2011) Creator of the [C](http://en.wikipedia.org/wiki/C_language) programming language and of [Unix](http://en.wikipedia.org/wiki/Unix)
 
 # 竜 **TatSu**
@@ -44,21 +44,21 @@ $ pip install TatSu
 
 竜 **TatSu** can be used as a library, much like [Python](http://python.org)'s `re`, by embedding grammars as strings and generating grammar models instead of generating [Python](http://python.org) code.
 
-  - `tatsu.compile(grammar, name=None, **kwargs)`
-    
+-   `tatsu.compile(grammar, name=None, **kwargs)`
+
     Compiles the grammar and generates a *model* that can subsequently be used for parsing input with.
 
-  - `tatsu.parse(grammar, input, **kwargs)`
-    
+-   `tatsu.parse(grammar, input, **kwargs)`
+
     Compiles the grammar and parses the given input producing an [AST](http://en.wikipedia.org/wiki/Abstract_syntax_tree) as result. The result is equivalent to calling:
-    
+
         model = compile(grammar)
         ast = model.parse(input)
-    
+
     Compiled grammars are cached for efficiency.
 
-  - `tatsu.to_python_sourcecode(grammar, name=None, filename=None, **kwargs)`
-    
+-   `tatsu.to_python_sourcecode(grammar, name=None, filename=None, **kwargs)`
+
     Compiles the grammar to the [Python](http://python.org) sourcecode that implements the parser.
 
 This is an example of how to use 竜 **TatSu** as a library:
@@ -150,7 +150,7 @@ For a detailed explanation of what 竜 **TatSu** is capable of, please see the [
 
 ## Questions?
 
-Please use the [\[tatsu\]](https://stackoverflow.com/tags/tatsu/info) tag on [StackOverflow](http://stackoverflow.com/tags/tatsu/info) for general Q\&A, and limit Github issues to bugs, enhancement proposals, and feature requests.
+Please use the [\[tatsu\]](https://stackoverflow.com/tags/tatsu/info) tag on [StackOverflow](http://stackoverflow.com/tags/tatsu/info) for general Q&A, and limit Github issues to bugs, enhancement proposals, and feature requests.
 
 ## Changes
 
diff --git a/tatsu/contexts.py b/tatsu/contexts.py
index a5af681..d40cf77 100644
--- a/tatsu/contexts.py
+++ b/tatsu/contexts.py
@@ -205,6 +205,7 @@ class ParseContext(object):
               semantics=None,
               trace=False,
               whitespace=None,
+              ignorecase=None,
               **kwargs):
         try:
             self.parseinfo = kwargs.pop('parseinfo', self.parseinfo)
@@ -215,6 +216,7 @@ class ParseContext(object):
                 semantics=semantics,
                 trace=trace if trace is not None else self.trace,
                 whitespace=whitespace if whitespace is not None else self.whitespace,
+                ignorecase=ignorecase,
                 **kwargs
             )
             rule = self._find_rule(rule_name)
@@ -848,7 +850,7 @@ class ParseContext(object):
     def _check_name(self, name=None):
         if name is None:
             name = str(self.last_node)
-        if self.ignorecase or self._tokenizer.ignorecase:
+        if self.ignorecase or self.tokenizer.ignorecase:
             name = name.upper()
         if name in self.keywords:
             raise FailedKeywordSemantics('"%s" is a reserved word' % name)
diff --git a/tatsu/grammars.py b/tatsu/grammars.py
index 8a90a8d..bb61bec 100644
--- a/tatsu/grammars.py
+++ b/tatsu/grammars.py
@@ -954,6 +954,7 @@ class Grammar(Model):
                  semantics=None,
                  filename='Unknown',
                  whitespace=None,
+                 ignorecase=None,
                  nameguard=None,
                  namechars=None,
                  left_recursion=None,
@@ -983,6 +984,10 @@ class Grammar(Model):
             whitespace = directives.get('whitespace')
         self.whitespace = whitespace
 
+        if ignorecase is None:
+            ignorecase = directives.get('ignorecase')
+        self.ignorecase = ignorecase
+
         if nameguard is None:
             nameguard = directives.get('nameguard')
         self.nameguard = nameguard
@@ -1008,6 +1013,8 @@ class Grammar(Model):
         self.eol_comments_re = eol_comments_re
 
         self.keywords = keywords or set()
+        if ignorecase:
+            self.keywords = {k.upper() for k in self.keywords}
 
         self._adopt_children(rules)
 
@@ -1082,6 +1089,7 @@ class Grammar(Model):
               trace=False,
               context=None,
               whitespace=None,
+              ignorecase=None,
               left_recursion=None,
               comments_re=None,
               eol_comments_re=None,
@@ -1106,6 +1114,7 @@ class Grammar(Model):
         nameguard = notnone(nameguard, self.nameguard)
         namechars = notnone(namechars, self.namechars)
         whitespace = notnone(whitespace, self.whitespace)
+        ignorecase = notnone(ignorecase, self.ignorecase)
         if whitespace:
             whitespace = re.compile(whitespace)
 
@@ -1116,6 +1125,7 @@ class Grammar(Model):
             semantics=semantics,
             trace=trace,
             whitespace=whitespace,
+            ignorecase=ignorecase,
             comments_re=comments_re,
             eol_comments_re=eol_comments_re,
             left_recursion=left_recursion,
diff --git a/tox.ini b/tox.ini
index f26f242..372bb77 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,7 +26,7 @@ deps =
 [flake8]
 ignore = N802, W504, W605
 max-line-length = 200
-max-complexity = 10
+max-complexity = 16
 exclude =
     parsers,
     docs,


Test patch:
diff --git a/test/grammar/keyword_test.py b/test/grammar/keyword_test.py
index d8574da..da65540 100644
--- a/test/grammar/keyword_test.py
+++ b/test/grammar/keyword_test.py
@@ -132,3 +132,29 @@ class KeywordTests(unittest.TestCase):
                 self.fail('accepted keyword "%s" as name' % k)
             except FailedParse as e:
                 self.assertTrue('"%s" is a reserved word' % k in str(e))
+
+    def test_ignorecase_keywords(self):
+        grammar = '''
+            @@ignorecase :: True
+            @@keyword :: if
+
+            start = rule ;
+
+            @name
+            rule = @:word if_exp $ ;
+
+            if_exp = 'if' digit ;
+
+            word = /\w+/ ;
+            digit = /\d/ ;
+        '''
+
+        model = compile(grammar, 'test')
+
+        model.parse('nonIF if 1', trace=True)
+
+        with self.assertRaises(FailedParse):
+            model.parse('i rf if 1', trace=True)
+
+        with self.assertRaises(FailedParse):
+            model.parse('IF if 1', trace=True)
diff --git a/test/parser_equivalence_test.py b/test/parser_equivalence_test.py
index dacaf60..337b608 100644
--- a/test/parser_equivalence_test.py
+++ b/test/parser_equivalence_test.py
@@ -3,6 +3,8 @@ import subprocess  # noqa
 import py_compile  # noqa
 from pathlib import Path
 
+import pytest
+
 from tatsu.tool import compile, gencode
 
 INPUT = """
@@ -38,11 +40,13 @@ GRAMMAR = """
         ;
 """
 
+
 def test_model_parse():
     model = compile(grammar=GRAMMAR)
     assert OUTPUT == model.parse(INPUT)
 
 
+@pytest.mark.skip('work in progress')
 def test_codegen_parse():
     init_filename = Path('./tmp/__init__.py')
     init_filename.touch(exist_ok=True)
@@ -65,7 +69,7 @@ def test_codegen_parse():
         #     }
         # ).decode()
         # print(output)
-        from tmp.parser import UnknownParser
+        from tmp.parser import UnknownParser  # pylint: disable=all
         output = UnknownParser().parse(INPUT)
         assert output == OUTPUT
     finally: