@@ -31,6 +31,7 @@ def __init__(self, config_file_path, report_path_override=None, verbosity=1, sou
3131 """
3232 self .groups = {}
3333 self .choices = {}
34+ self .optional_groups = []
3435 self .annotation_tokens = []
3536 self .annotation_regexes = []
3637 self .mgr = None
@@ -108,19 +109,36 @@ def _is_choice_group(self, token_or_group):
108109 Returns:
109110 True if the type of the annotation is correct for a choice group, otherwise False
110111 """
111- return isinstance (token_or_group , dict )
112+ return isinstance (token_or_group , dict ) and "choices" in token_or_group
113+
114+ def _is_optional_group (self , token_or_group ):
115+ """
116+ Determine if an annotation is an optional group.
117+
118+ Args:
119+ token_or_group: The annotation being checked
120+
121+ Returns:
122+ True if the annotation is optional, otherwise False.
123+ """
124+ return isinstance (token_or_group , dict ) and bool (token_or_group .get ("optional" ))
112125
113126 def _is_annotation_token (self , token_or_group ):
114127 """
115- Determine if an annotation is a free-form text type .
128+ Determine if an annotation has the right format .
116129
117130 Args:
118131 token_or_group: The annotation being checked
119132
120133 Returns:
121134 True if the type of the annotation is correct for a text type, otherwise False
122135 """
123- return token_or_group is None
136+ if token_or_group is None :
137+ return True
138+ if isinstance (token_or_group , dict ):
139+ # If annotation is a dict, only a few keys are tolerated
140+ return set (token_or_group .keys ()).issubset ({"choices" , "optional" })
141+ return False
124142
125143 def _add_annotation_token (self , token ):
126144 if token in self .annotation_tokens :
@@ -172,13 +190,15 @@ def _configure_group(self, group_name, group):
172190 for annotation_token in annotation :
173191 annotation_value = annotation [annotation_token ]
174192
193+ # Otherwise it should be a text type, if not then error out
194+ if not self ._is_annotation_token (annotation_value ):
195+ raise ConfigurationException (f'{ annotation } is an unknown annotation type.' )
175196 # The annotation comment is a choice group
176197 if self ._is_choice_group (annotation_value ):
177198 self ._configure_choices (annotation_token , annotation_value )
178-
179- # Otherwise it should be a text type, if not then error out
180- elif not self ._is_annotation_token (annotation_value ):
181- raise ConfigurationException (f'{ annotation } is an unknown annotation type.' )
199+ # The annotation comment is not mandatory
200+ if self ._is_optional_group (annotation_value ):
201+ self .optional_groups .append (annotation_token )
182202
183203 self .groups [group_name ].append (annotation_token )
184204 self ._add_annotation_token (annotation_token )
@@ -370,7 +390,7 @@ def _check_results_choices(self, annotation):
370390 else :
371391 self ._add_annotation_error (
372392 annotation ,
373- 'No choices found for "{}". Expected one of {}.' .format (token , self .config .choices [token ])
393+ 'no value found for "{}". Expected one of {}.' .format (token , self .config .choices [token ])
374394 )
375395 return None
376396
@@ -419,17 +439,86 @@ def check_results(self, all_results):
419439
420440 # Spin through the search results
421441 for filename in all_results :
422- current_group = None
423- found_group_tokens = []
424- for annotation in all_results [filename ]:
425- current_group = self .check_annotation (annotation , current_group , found_group_tokens )
442+ for annotations in self .iter_groups (all_results [filename ]):
443+ self .check_group (annotations )
444+ return not self .errors
426445
427- if current_group :
428- self .errors .append ('File("{}") finished with an incomplete group {}!' .format (filename , current_group ))
446+ def iter_groups (self , annotations ):
447+ """
448+ Iterate on groups of annotations. Annotations are considered as a group when they all have the same
449+ `line_number`, which should point to the beginning of the annotation group.
429450
430- return not self .errors
451+ Yield:
452+ annotations (annotation list)
453+ """
454+ current_group = []
455+ current_line_number = None
456+ for annotation in annotations :
457+ line_number = annotation ["line_number" ]
458+ line_number_changed = line_number != current_line_number
459+ if line_number_changed :
460+ if current_group :
461+ yield current_group
462+ current_group .clear ()
463+ current_group .append (annotation )
464+ current_line_number = line_number
465+
466+ if current_group :
467+ yield current_group
431468
432- def check_annotation (self , annotation , current_group , found_group_tokens ):
469+ def check_group (self , annotations ):
470+ """
471+ Perform several linting checks on a group of annotations:
472+
473+ - Choice fields should have a valid value
474+ - Annotation tokens are valid
475+ - There is no duplicate
476+ - All non-optional tokens are present
477+ """
478+ found_tokens = set ()
479+ group_tokens = []
480+ group_name = None
481+ for annotation in annotations :
482+ token = annotation ["annotation_token" ]
483+ if not group_name :
484+ group_name = self ._get_group_for_token (token )
485+ if group_name :
486+ group_tokens = self .config .groups [group_name ]
487+
488+ # Check if choice field
489+ self ._check_results_choices (annotation )
490+
491+ # Check token belongs to group
492+ if group_name :
493+ if token not in group_tokens :
494+ self ._add_annotation_error (
495+ annotation ,
496+ "'{}' token does not belong to group '{}'. Expected one of: {}" .format (
497+ token ,
498+ group_name ,
499+ group_tokens
500+ )
501+ )
502+
503+ # Check for duplicates
504+ if token in found_tokens :
505+ self ._add_annotation_error (
506+ annotation ,
507+ "found duplicate token '{}'" .format (token )
508+ )
509+ if group_name :
510+ found_tokens .add (token )
511+
512+ # Check for missing tokens
513+ for token in group_tokens :
514+ if token not in self .config .optional_groups :
515+ if token not in found_tokens :
516+ self ._add_annotation_error (
517+ annotations [0 ],
518+ "missing non-optional annotation: '{}'" .format (token )
519+ )
520+
521+ def check_annotation (self , annotation , current_group ):
433522 """
434523 Check an annotation and add annotation errors when necessary.
435524
@@ -459,39 +548,21 @@ def check_annotation(self, annotation, current_group, found_group_tokens):
459548 )
460549 )
461550 current_group = None
462- found_group_tokens .clear ()
463- elif token in found_group_tokens :
464- # Check for duplicate tokens
465- self ._add_annotation_error (
466- annotation ,
467- '"{}" is already in the group that starts with "{}"' .format (token , current_group )
468- )
469- current_group = None
470- found_group_tokens .clear ()
471551 else :
472552 # Token is correct
473553 self .echo .echo_vv ('Adding "{}", line {} to group {}' .format (
474554 token ,
475555 annotation ['line_number' ],
476556 current_group
477557 ))
478- found_group_tokens .append (token )
479558 else :
480559 current_group = self ._get_group_for_token (token )
481560 if current_group :
482561 # Start a new group
483- found_group_tokens .clear ()
484- found_group_tokens .append (token )
485562 self .echo .echo_vv ('Starting new group for "{}" token "{}", line {}' .format (
486563 current_group , token , annotation ['line_number' ])
487564 )
488565
489- # If we have all members, this group is done
490- if current_group and len (found_group_tokens ) == len (self .config .groups [current_group ]):
491- self .echo .echo_vv ("Group complete!" )
492- current_group = None
493- found_group_tokens .clear ()
494-
495566 return current_group
496567
497568 def _add_annotation_error (self , annotation , message ):
0 commit comments