-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: [FC-0074] add inline code annotation linter for Open edX Events #478
Merged
Merged
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
cd01776
feat: add inline code-annotations for Open edX Events
mariajgrimaldi 3dc1a67
fix: make events annotation an optional plugin
mariajgrimaldi e60eb11
refactor: drop `.. event_status` unnecessary annotation
mariajgrimaldi 2c6735e
refactor: rename events_annotations.yaml to the correct config filename
mariajgrimaldi 3309c2b
refactor: address quality errors
mariajgrimaldi 8d1079e
refactor: address quality errors
mariajgrimaldi d1adeda
refactor!: check whether event type and data matches definition for g…
mariajgrimaldi dadaa92
refactor: consider event name in linting strategy
mariajgrimaldi e84bc86
refactor: address quality errors
mariajgrimaldi c14e40c
docs: update documentation for new release
mariajgrimaldi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
""" | ||
edx_lint events_annotation module (optional plugin for events inline code-annotations | ||
checks). | ||
|
||
Add this to your pylintrc:: | ||
load-plugins=edx_lint.pylint.events_annotation | ||
""" | ||
|
||
from .events_annotation_check import register_checkers | ||
|
||
register = register_checkers |
182 changes: 182 additions & 0 deletions
182
edx_lint/pylint/events_annotation/events_annotation_check.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
""" | ||
Pylint plugin: checks that Open edX Events are properly annotated. | ||
""" | ||
|
||
from astroid.nodes.node_classes import Const, Name | ||
from pylint.checkers import utils | ||
|
||
from edx_lint.pylint.annotations_check import AnnotationBaseChecker, check_all_messages | ||
from edx_lint.pylint.common import BASE_ID | ||
|
||
|
||
def register_checkers(linter): | ||
""" | ||
Register checkers. | ||
""" | ||
linter.register_checker(EventsAnnotationChecker(linter)) | ||
|
||
|
||
class EventsAnnotationChecker(AnnotationBaseChecker): | ||
""" | ||
Perform checks on events annotations. | ||
""" | ||
|
||
CONFIG_FILENAMES = ["openedx_events_annotations.yaml"] | ||
|
||
name = "events-annotations" | ||
|
||
NO_TYPE_MESSAGE_ID = "event-no-type" | ||
NO_NAME_MESSAGE_ID = "event-no-name" | ||
NO_DATA_MESSAGE_ID = "event-no-data" | ||
NO_STATUS_MESSAGE_ID = "event-no-status" | ||
NO_DESCRIPTION_MESSAGE_ID = "event-empty-description" | ||
MISSING_ANNOTATION = "event-missing-annotation" | ||
|
||
msgs = { | ||
("E%d80" % BASE_ID): ( | ||
"event annotation has no type", | ||
NO_TYPE_MESSAGE_ID, | ||
"Events annotations type must be present and be the first annotation", | ||
), | ||
("E%d81" % BASE_ID): ( | ||
"event annotation (%s) has no name", | ||
NO_NAME_MESSAGE_ID, | ||
"Events annotations name must be present", | ||
), | ||
("E%d82" % BASE_ID): ( | ||
"event annotation (%s) has no data argument", | ||
NO_DATA_MESSAGE_ID, | ||
"Events annotations must include data argument", | ||
), | ||
("E%d83" % BASE_ID): ( | ||
"event annotation (%s) has no status", | ||
NO_STATUS_MESSAGE_ID, | ||
"Events annotations must include the status of event", | ||
), | ||
("E%d84" % BASE_ID): ( | ||
"event annotation (%s) does not have a description", | ||
NO_DESCRIPTION_MESSAGE_ID, | ||
"Events annotations must include a short description", | ||
), | ||
("E%d85" % BASE_ID): ( | ||
"missing event annotation", | ||
MISSING_ANNOTATION, | ||
( | ||
"When an Open edX event object is created, a corresponding annotation must be present above in the" | ||
" same module and with a matching name", | ||
) | ||
), | ||
} | ||
|
||
EVENT_CLASS_NAMES = ["OpenEdxPublicSignal"] | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.current_module_annotated_event_names = set() | ||
self.current_module_annotation_group_line_numbers = [] | ||
|
||
@check_all_messages(msgs) | ||
def visit_module(self, node): | ||
""" | ||
Run all checks on a single module. | ||
""" | ||
self.check_module(node) | ||
|
||
def leave_module(self, _node): | ||
self.current_module_annotated_event_names.clear() | ||
self.current_module_annotation_group_line_numbers.clear() | ||
|
||
def check_annotation_group(self, search, annotations, node): | ||
""" | ||
Perform checks on a single annotation group. | ||
""" | ||
if not annotations: | ||
return | ||
|
||
event_type = "" | ||
event_name = "" | ||
event_data = "" | ||
event_description = "" | ||
line_number = None | ||
for annotation in annotations: | ||
if line_number is None: | ||
line_number = annotation["line_number"] | ||
self.current_module_annotation_group_line_numbers.append(line_number) | ||
if annotation["annotation_token"] == ".. event_type:": | ||
event_type = annotation["annotation_data"] | ||
elif annotation["annotation_token"] == ".. event_name:": | ||
event_name = annotation["annotation_data"] | ||
self.current_module_annotated_event_names.add(event_name) | ||
elif annotation["annotation_token"] == ".. event_data:": | ||
event_data = annotation["annotation_data"] | ||
elif annotation["annotation_token"] == ".. event_description:": | ||
event_description = annotation["annotation_data"] | ||
|
||
if not event_type: | ||
self.add_message( | ||
self.NO_TYPE_MESSAGE_ID, | ||
node=node, | ||
line=line_number, | ||
) | ||
|
||
if not event_name: | ||
self.add_message( | ||
self.NO_NAME_MESSAGE_ID, | ||
args=(event_type,), | ||
node=node, | ||
line=line_number, | ||
) | ||
|
||
if not event_data: | ||
self.add_message( | ||
self.NO_DATA_MESSAGE_ID, | ||
args=(event_type,), | ||
node=node, | ||
line=line_number, | ||
) | ||
|
||
if not event_description: | ||
self.add_message( | ||
self.NO_DESCRIPTION_MESSAGE_ID, | ||
args=(event_type,), | ||
node=node, | ||
line=line_number, | ||
) | ||
|
||
@utils.only_required_for_messages(MISSING_ANNOTATION) | ||
def visit_call(self, node): | ||
""" | ||
Check for missing annotations. | ||
""" | ||
if self.is_annotation_missing(node): | ||
self.add_message( | ||
self.MISSING_ANNOTATION, | ||
node=node, | ||
) | ||
|
||
def is_annotation_missing(self, node): | ||
mariajgrimaldi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Check whether the node corresponds to a toggle instance creation. if yes, check that it is annotated. | ||
""" | ||
if ( | ||
not isinstance(node.func, Name) | ||
or node.func.name not in self.EVENT_CLASS_NAMES | ||
): | ||
return False | ||
|
||
if not self.current_module_annotation_group_line_numbers: | ||
# There are no annotations left | ||
return True | ||
|
||
annotation_line_number = self.current_module_annotation_group_line_numbers[0] | ||
if annotation_line_number > node.tolineno: | ||
# The next annotation is located after the current node | ||
return True | ||
self.current_module_annotation_group_line_numbers.pop(0) | ||
|
||
# Check literal event name arguments | ||
if node.args and isinstance(node.args[0], Const) and isinstance(node.args[0].value, str): | ||
event_name = node.args[0].value | ||
if event_name not in self.current_module_annotated_event_names: | ||
return True | ||
return False |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current use of
self.current_module_annotation_group_line_numbers
for tracking annotation lines looks functional but may be brittle if annotations are not perfectly aligned with the corresponding events so this reliance on order may lead to false positives or negatives in certain scenarios.I think we could consider using a mapping from line numbers to event names (something like self.current_module_annotations = {line_number: event_name}) to directly associate each annotation with its corresponding event, this could make the logic more robust and reduce potential issues caused by misalignment or unexpected ordering
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion! This implementation was inspired by the toggle annotations checker, here: https://github.com/openedx/edx-lint/blob/master/edx_lint/pylint/annotations_check.py#L329-L519 and then modified to fit the events inline code-annotations.
As far as I understand, annotations are visited in
check_annotation_group
using the ASTWalker, which visits annotation groups (starting with# ..
) for each module. It finds the line where the annotation group starts, saves it in a queue-like list and saves an identifier, in this case, the event name for the annotation. Thenis_annotation_missing
is called for eachOpenEdxPublicSignal
instantiation (calledcall node
). Since they are also visited using the same ASTWalker, the nodes are traversed in the same order, so the annotation groups should match the call node:That's why I don't think we would have an issue with unexpected ordering since we're traversing the modules using the same strategy. Also, since we are using the line number queue of each annotation group as our stopping condition, then we'd need the dict to behave like a queue so
is_annotation_missing
stops when needed.Those are the main reasons why I think changing the set and queue for a dict would add more overhead than leaving it as it is. Let me know if you disagree or have any other suggestions considering what I mentioned.
EDIT: maybe something like this could happen, where the event names are switched and the linter won't raise any issues. I'll try to invest some time into figuring out how to cover these cases, I'll let you know what I find!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Alec4r: I think I've addressed your concerns with these commits:
_is_annotation_missing_or_incorrect
, the current annotation group should match the event init arguments (since they're traversed in order) so the check is successful: d1adedaLet me know what you think!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mariajgrimaldi looks good for me.