206206 handle_and_manage ,
207207 manage ,
208208 sub_all ,
209+ same_line ,
209210 get_cache_items_for ,
210211 clear_packrat_cache ,
211212 add_packrat_cache_items ,
@@ -604,8 +605,8 @@ def reset(self, keep_state=False, filename=None):
604605 # but always overwrite temp_vars_by_key since they store locs that will be invalidated
605606 self .temp_vars_by_key = {}
606607 self .parsing_context = defaultdict (list )
607- self .parsing_context ["final_vars" ].append (set () ) # initialize module-level final scope
608- self .name_info = defaultdict (lambda : {"imported" : [] , "referenced" : [] , "assigned" : [] })
608+ self .parsing_context ["final_vars" ].append ({} ) # initialize module-level final scope
609+ self .name_info = defaultdict (lambda : {"imported" : set () , "referenced" : set () , "assigned" : set () })
609610 self .star_import = False
610611 self .kept_lines = []
611612 self .num_lines = 0
@@ -635,7 +636,7 @@ def inner_environment(self, ln=None):
635636 skips , self .skips = self .skips , []
636637 docstring , self .docstring = self .docstring , ""
637638 parsing_context , self .parsing_context = self .parsing_context , defaultdict (list )
638- self .parsing_context ["final_vars" ].append (set () ) # initialize module-level final scope
639+ self .parsing_context ["final_vars" ].append ({} ) # initialize module-level final scope
639640 kept_lines , self .kept_lines = self .kept_lines , []
640641 num_lines , self .num_lines = self .num_lines , 0
641642 remaining_original , self .remaining_original = self .remaining_original , None
@@ -1051,19 +1052,28 @@ def strict_err_or_warn(self, *args, **kwargs):
10511052 """Raises an error if in strict mode, otherwise raises a warning. Usage:
10521053 self.strict_err_or_warn(message, original, loc)
10531054 """
1055+ raise_err_func = kwargs .pop ("raise_err_func" , None )
10541056 internal_assert ("extra" not in kwargs , "cannot pass extra=... to strict_err_or_warn" )
10551057 if self .strict :
10561058 kwargs ["extra" ] = "remove --strict to downgrade to a warning"
1057- raise self .make_err (CoconutStyleError , * args , ** kwargs )
1059+ if raise_err_func is None :
1060+ raise self .make_err (CoconutStyleError , * args , ** kwargs )
1061+ else :
1062+ return raise_err_func (CoconutStyleError , * args , ** kwargs )
10581063 else :
10591064 self .syntax_warning (* args , ** kwargs )
10601065
10611066 def strict_qa_error (self , msg , original , loc , ** kwargs ):
1062- """Strict error or warn an error that should be disabled by a NOQA comment."""
1067+ """Strict error or warn an error that should be disabled by a NOQA comment.
1068+
1069+ Use strict_qa_error when the error is non-greedy and the comment can be handled first;
1070+ use strict_err_or_warn when the error is greedy, which means the comment won't have
1071+ been recorded yet and so we won't be able to check if it contains NOQA.
1072+ """
10631073 ln = self .adjust (lineno (loc , original ))
10641074 comment = self .reformat (" " .join (self .comments [ln ]), ignore_errors = True )
10651075 if not self .noqa_regex .search (comment ):
1066- self .strict_err_or_warn (
1076+ return self .strict_err_or_warn (
10671077 msg + " (add '# NOQA' to suppress)" ,
10681078 original ,
10691079 loc ,
@@ -1182,11 +1192,12 @@ def wrap_error(self, error_maker):
11821192
11831193 def raise_or_wrap_error (self , * args , ** kwargs ):
11841194 """Raise or defer if USE_COMPUTATION_GRAPH else wrap."""
1195+ always_wrap = kwargs .pop ("always_wrap" , False )
11851196 error_maker = partial (self .make_err , * args , ** kwargs )
11861197 if not USE_COMPUTATION_GRAPH :
11871198 return self .wrap_error (error_maker )
11881199 # differently-ordered any ofs can push these errors earlier than they should be, requiring us to defer them
1189- elif use_adaptive_any_of or reverse_any_of :
1200+ elif always_wrap or use_adaptive_any_of or reverse_any_of :
11901201 return ExceptionNode (error_maker )
11911202 else :
11921203 raise error_maker ()
@@ -4081,7 +4092,7 @@ def import_handle(self, original, loc, tokens):
40814092 self .syntax_warning ("[from *] import * is a Coconut Easter egg and should not be used in production code" , original , loc )
40824093 return special_starred_import_handle (imp_all = bool (imp_from ))
40834094 for imp_name in imported_names :
4084- self .name_info [imp_name ]["imported" ].append (loc )
4095+ self .name_info [imp_name ]["imported" ].add (loc )
40854096 return self .universal_import (loc , imports , imp_from = imp_from )
40864097
40874098 def complex_raise_stmt_handle (self , loc , tokens ):
@@ -4369,7 +4380,7 @@ def stmt_lambdef_handle(self, original, loc, tokens):
43694380 got_kwds , params , typedef , stmts_toks , followed_by = tokens
43704381
43714382 if followed_by == "," :
4372- self .strict_qa_error ("found statement lambda followed by comma; this isn't recommended as it can be unclear whether the comma is inside or outside the lambda (just wrap the lambda in parentheses)" , original , loc )
4383+ self .strict_err_or_warn ("found statement lambda followed by comma; this isn't recommended as it can be unclear whether the comma is inside or outside the lambda (just wrap the lambda in parentheses)" , original , loc )
43734384 else :
43744385 internal_assert (followed_by == "" , "invalid stmt_lambdef followed_by" , followed_by )
43754386
@@ -5090,7 +5101,7 @@ def type_param_handle(self, original, loc, tokens):
50905101 if bound_op is not None :
50915102 self .internal_assert (bound_op_type in ("bound" , "constraint" ), original , loc , "invalid type_param bound_op" , bound_op )
50925103 if bound_op == "<=" :
5093- self .strict_qa_error (
5104+ self .strict_err_or_warn (
50945105 "use of " + repr (bound_op ) + " as a type parameter " + bound_op_type + " declaration operator is deprecated (Coconut style is to use '<:' for bounds and ':' for constaints)" ,
50955106 original ,
50965107 loc ,
@@ -5256,7 +5267,7 @@ def class_manage(self, original, loc, item):
52565267 try :
52575268 # handles support for class type variables
52585269 with self .type_alias_stmt_manage ():
5259- with self .add_to_parsing_context ("final_vars" , set () ):
5270+ with self .add_to_parsing_context ("final_vars" , {} ):
52605271 yield
52615272 finally :
52625273 cls_stack .pop ()
@@ -5270,7 +5281,7 @@ def func_manage(self, original, loc, item):
52705281 try :
52715282 # handles support for function type variables
52725283 with self .type_alias_stmt_manage ():
5273- with self .add_to_parsing_context ("final_vars" , set () ):
5284+ with self .add_to_parsing_context ("final_vars" , {} ):
52745285 yield
52755286 finally :
52765287 if cls_context is not None :
@@ -5315,27 +5326,13 @@ def name_handle(self, original, loc, tokens, assign=False, classname=False, expr
53155326 if self .disable_name_check :
53165327 return name
53175328
5318- # raise_or_wrap_error for all errors here to make sure we don't
5319- # raise spurious errors if not using the computation graph
5320-
5321- # final variable checking
5322- final_vars = self .current_parsing_context ("final_vars" )
5323- self .internal_assert (final_vars is not None , original , loc , "no final_vars context" )
5324- if (
5325- assign
5326- and not expr_setname
5327- and not escaped
5328- and name in final_vars
5329- ):
5330- return self .raise_or_wrap_error (
5331- CoconutSyntaxError ,
5332- "cannot reassign final variable '{name}'" .format (name = name ),
5333- original ,
5334- loc ,
5335- extra = "use explicit '\\ {name}' syntax to bypass final checking" .format (name = name ),
5336- )
5337- if is_final :
5338- final_vars .add (name )
5329+ # these are all handled greedily, so they should always be wrapped
5330+ is_greedy = classname or expr_setname or is_final
5331+ # use this for all errors here to make sure we don't raise spurious errors
5332+ local_raise_or_wrap_error = partial (
5333+ self .raise_or_wrap_error ,
5334+ always_wrap = is_greedy ,
5335+ )
53395336
53405337 # register non-mid-expression variable assignments inside of where statements for later mangling
53415338 if assign and not expr_setname :
@@ -5365,42 +5362,109 @@ def name_handle(self, original, loc, tokens, assign=False, classname=False, expr
53655362 # a reparse of a setname in a typevar, or not a typevar at all
53665363 if typevar_info ["typevar_locs" ].get (name , None ) != loc :
53675364 if assign :
5368- return self . raise_or_wrap_error (
5365+ return local_raise_or_wrap_error (
53695366 CoconutSyntaxError ,
53705367 "cannot reassign type variable '{name}'" .format (name = name ),
53715368 original ,
53725369 loc ,
53735370 extra = "use explicit '\\ {name}' syntax if intended" .format (name = name ),
53745371 )
5372+ # note that this is the one case where we return early that isn't an error
53755373 return typevars [name ]
53765374
5375+ # track assigned/referenced early so that even if we return early with an
5376+ # error, the name is marked as used (e.g. for unused import checking)
53775377 if assign :
5378- self .name_info [name ]["assigned" ].append (loc )
5378+ is_new = loc not in self .name_info [name ]["assigned" ]
5379+ self .name_info [name ]["assigned" ].add (loc )
53795380 else :
5380- self .name_info [name ]["referenced" ].append (loc )
5381+ is_new = loc not in self .name_info [name ]["referenced" ]
5382+ self .name_info [name ]["referenced" ].add (loc )
5383+
5384+ is_class_attr = (
5385+ self .current_parsing_context ("class" )
5386+ and not self .in_method
5387+ and (
5388+ # for classnames, we need special handling for nested classes
5389+ True if not classname
5390+ # - outer class (len == 1): this is the name of the class, not a class attr
5391+ else False if len (self .parsing_context ["class" ]) <= 1
5392+ # - class defined directly in a method (immediate parent has in_method):
5393+ # not a class attr, this is a variable in the method's local scope
5394+ else False if self .parsing_context ["class" ][- 2 ].get ("in_method" )
5395+ # - nested class as attribute (immediate parent does NOT have in_method):
5396+ # this is a class attr just like any other
5397+ else True
5398+ )
5399+ )
53815400
53825401 if (
53835402 assign
53845403 and not escaped
5404+ # if we're creating a class attribute, then we're not actually shadowing anything
5405+ and not is_class_attr
53855406 # if we're not using the computation graph, then name is handled
53865407 # greedily, which means this might be an invalid parse, in which
5387- # case we can't be sure this is actually shadowing a builtin
5388- and USE_COMPUTATION_GRAPH
5389- # classnames and expr_setnames are handled greedily, so ditto the above
5390- and not (classname or expr_setname )
5391- and name in all_builtins
5408+ # case we can't be sure this is actually shadowing a builtin;
5409+ # BUT if we're on strict mode, then it's an actual error, rather
5410+ # than a warning, which means we can just wrap it
5411+ and (
5412+ # in strict mode, errors are wrapped and we should always do that
5413+ self .strict
5414+ # in non-strict mode, only check when using computation graph
5415+ # and not for greedy handlers (to avoid spurious warnings)
5416+ # and only if it's a new assignment (to avoid duplicate warnings)
5417+ or (is_new and not is_greedy and USE_COMPUTATION_GRAPH )
5418+ )
53925419 ):
5393- self .strict_qa_error (
5394- "assignment shadows builtin '{name}' (use explicit '\\ {name}' syntax when purposefully assigning to builtin names)" .format (name = name ),
5420+ if name in all_builtins :
5421+ err = self .strict_err_or_warn (
5422+ "assignment shadows builtin '{name}' (use explicit '\\ {name}' syntax when purposefully assigning to builtin names)" .format (name = name ),
5423+ original ,
5424+ loc ,
5425+ raise_err_func = local_raise_or_wrap_error ,
5426+ )
5427+ if err is not None :
5428+ return err
5429+ # Only check for shadowing if there are imports on different lines;
5430+ # if all imports are on the same line as the current loc, this is
5431+ # the import statement itself, not an assignment shadowing an import
5432+ if any (not same_line (original , loc , imp_loc ) for imp_loc in self .name_info [name ]["imported" ]):
5433+ err = self .strict_err_or_warn (
5434+ "assignment shadows imported name '{name}' (use explicit '\\ {name}' syntax when purposefully redefining imported names)" .format (name = name ),
5435+ original ,
5436+ loc ,
5437+ raise_err_func = local_raise_or_wrap_error ,
5438+ )
5439+ if err is not None :
5440+ return err
5441+
5442+ # final variable checking
5443+ final_vars = self .current_parsing_context ("final_vars" )
5444+ self .internal_assert (final_vars is not None , original , loc , "no final_vars context" )
5445+ if (
5446+ assign
5447+ and not escaped
5448+ and not expr_setname
5449+ and name in final_vars
5450+ and final_vars [name ] != loc # allow reassign in same loc (speculative parsing duplicate)
5451+ ):
5452+ return local_raise_or_wrap_error (
5453+ CoconutSyntaxError ,
5454+ "cannot reassign final variable '{name}'" .format (name = name ),
53955455 original ,
53965456 loc ,
5457+ extra = "use explicit '\\ {name}' syntax to bypass final checking" .format (name = name ),
53975458 )
5459+ # only mark as final after all checks pass
5460+ if is_final :
5461+ final_vars [name ] = loc
53985462
53995463 if name == "exec" :
54005464 if self .target .startswith ("3" ):
54015465 return name
54025466 elif assign :
5403- return self . raise_or_wrap_error (
5467+ return local_raise_or_wrap_error (
54045468 CoconutTargetError ,
54055469 "found Python-3-only assignment to 'exec' as a variable name" ,
54065470 original ,
@@ -5417,7 +5481,7 @@ def name_handle(self, original, loc, tokens, assign=False, classname=False, expr
54175481 else :
54185482 return name
54195483 elif not escaped and name .startswith (reserved_prefix ) and name not in self .operators :
5420- return self . raise_or_wrap_error (
5484+ return local_raise_or_wrap_error (
54215485 CoconutSyntaxError ,
54225486 "variable names cannot start with reserved prefix '{prefix}' (use explicit '\\ {name}' syntax if intending to access Coconut internals)" .format (prefix = reserved_prefix , name = name ),
54235487 original ,
0 commit comments