-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Command linter #5952
Command linter #5952
Conversation
View a preview at https://prompt.ws/r/Azure/azure-cli/5952 |
493e557
to
eb1eb50
Compare
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.
Huge step in the right direction!
azure-cli.pyproj
Outdated
<PathEnvironmentVariable> | ||
</PathEnvironmentVariable> | ||
<Architecture>Amd64</Architecture> | ||
</Interpreter> |
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.
Revert these changes.
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.
oops
|
||
print('Initializing linter with command table and help files...') | ||
# setup CLI to enable command loader | ||
az_cli = TestCli() |
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.
Why TestCli instead of the regular CLI?
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.
Good question
create_invoker_and_load_cmds_and_args(az_cli) | ||
loaded_help = get_all_help(az_cli) | ||
command_table = az_cli.invocation.commands_loader.command_table | ||
add_id_parameters(None, cmd_tbl=command_table) |
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.
Shouldn't create_invoke_and_load_cmds_and_args
do this?
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.
It doesn't...though maybe it should
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.
Yes, it should...
I mean, it is really loading the commands and args if it leaves args out?
if rule_type in self._rules: | ||
self._rules.get(rule_type).append(rule_callable) | ||
|
||
def mark_rule_failure(self): |
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.
Why public?
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.
Because this needs to be exposed to the rules themselves. Each rule is responsible for alerting the linter if it fails.
def get_parameter_exclusions(self, command_name): | ||
return self._exclusions.get('params').get(command_name, {}) | ||
|
||
def add_rule(self, rule_type, rule_callable): |
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.
Why public?
|
||
@command_rule('Checking for --ids parameter in list commands...') | ||
def no_ids_for_list_commands_rule(linter, command_name): | ||
if command_name.split()[-1] == 'list' and 'ids' in linter.get_command_parameters(command_name): |
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.
I would expect command_name
here to be "list" automatically and not have to do this split. Consider having linter
, command_name
and command_path
for these rule types.
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 command_name (as a string) is used as keys to access lots of different information in the linter and will likely be used by almost every rule.
The command_path is not as useful except for identifying a particular aspect of a command (i.e. checking for list or update commands) which seems more of a situational use. I would also like to avoid duplication in the arguments given to the rules.
{ | ||
"help_entries": { | ||
"example_entry": [ | ||
"unrecognized_help_entry_rule" |
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.
I think we should definitely reconsider this. A single dumping ground for all exceptions would, I think, grow unwieldy. I think, as Derek suggested, having an exception file per module/extension is the better way to go.
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.
Hmm, that wouldn't be applicable to the help_entry rules and it seems like a lot of extra files that may only have one rule exception in them. It would be the same information just scattered throughout the modules.
Having one source gives the benefit of making it very easy to quickly search for and add new exclusions. Maybe we can save this discussion when we have more exclusions and see which is more usable?
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.
This piece should be vetted through the whole team, but I see some of your points.
I would also add that JSON is a pretty unfriendly format and would recommend something else (YAML, config format, etc.)
|
||
@help_file_entry_rule('Checking unrecognized commands and command-groups in help...') | ||
def unrecognized_help_entry_rule(linter, help_entry): | ||
if help_entry not in linter.get_commands().union(linter.get_command_groups()): |
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.
Since help entry rules would often be concerned with this "command or group" scenario, perhaps it would make sense to precompile this list in the linter so you don't have to keep creating this union, testing it, and throwing it away. Maybe something like:
def find_command_or_group_help(self, name):
""" Return the help entry for the specified command or command group, or None if not found. """
return self._master_help.get(name, None)
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.
I'll do two condition checks to avoid this. I don't foresee many rules needing access to both.
Thanks
def faulty_help_type_rule(linter, help_entry): | ||
mark = True | ||
if linter.get_help_entry_type(help_entry) != 'group' and help_entry in linter.get_command_groups(): | ||
print('--Help-Entry: `%s`- Command-group should be help-type `group`.' % help_entry) |
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.
Why don't you just mark failure here?
if linter.get_help_entry_type(help_entry) != 'group' and help_entry in linter.get_command_groups(): | ||
print('--Help-Entry: `%s`- Command-group should be help-type `group`.' % help_entry) | ||
elif linter.get_help_entry_type(help_entry) != 'command' and help_entry in linter.get_commands(): | ||
print('--Help-Entry: `%s`- Found in command table but is not of help-type `command`.' % help_entry) |
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.
And here? If the failure and message were combined, this would be a lot more succinct.
|
||
def _is_group(parser): | ||
return getattr(parser, '_subparsers', None) is not None \ | ||
or getattr(parser, 'choices', None) is not None |
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.
Would prefer this change in a separate file like help_util.py
because none of this is actually used by the CLI product yet other methods in here are used by the CLI in actual command usage.
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.
I can do that.
I'll make interactive leverage this soon so that it is used by other areas in the CLI.
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.
It would be good to connect with the VSCode guys as well. They need some public functions like this to insulate their extension form implementation changes within the CLI that cause their stuff to break.
@@ -3,6 +3,10 @@ | |||
Release History | |||
=============== | |||
|
|||
0.3.19 | |||
++++++ | |||
* Add id parameters to be used by completers. |
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.
What does this mean?
Can you add more info of make it clearer?
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.
I'll change this to "Allow completions for --ids parameters"
from pkgutil import iter_modules | ||
|
||
|
||
class Linter(): |
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.
Classes should inherit from object
https://stackoverflow.com/questions/4015417/python-class-inherits-object
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.
It may be worth trying the linter on Python 2 locally to see if it works there also.
Since the CLI supports py2 and 3, our dev tools should also ideally.
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.
works in python2 :)
self._exit_code = 1 | ||
|
||
def get_exit_code(self): | ||
return self._exit_code |
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.
In Python, you don't need all these get_X(self)
methods.
You can do this:
@property
def exit_code(self):
return self._exit_code
At least this is fine for the ones that only have self
as an argument.
def run(self, run_params=None, run_commands=None, run_command_groups=None, run_help_files_entries=None): | ||
paths = import_module('automation.cli_linter.rules').__path__ | ||
exclusion_path = os.path.join(paths[0], 'exclusions.json') | ||
self._exclusions = json.load(open(exclusion_path)) |
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.
If there is no exclusions file, does this still work?
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.
Definitely not, I can make it so it does.
@command_group_rule('Checking missing help for command-groups...') | ||
def missing_group_help_rule(linter, command_group_name): | ||
if not linter.get_command_group_help(command_group_name): | ||
print('--Command-Group: `%s`- Missing help.' % command_group_name) |
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.
I'm not convinced of the linter.mark_rule_failure()
pattern.
Python has Exceptions so we should leverage that.
For example, default a base class LinterException
and allow others to subclass this exception type.
Then at some central place, you have a try/ except LinterException
then let the exception print whatever details it needs and when you get at least one LinterException
, you know to exit with code 1.
.travis.yml
Outdated
@@ -55,3 +59,5 @@ jobs: | |||
python: 3.6 | |||
env: PURPOSE='Automation Docker' | |||
if: repo = Azure/azure-cli and type = push | |||
allow_failures: | |||
- env: PURPOSE='Lint Command Table and Help' |
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.
It's questionable why we whould really want a build stage that allows failures. I think historically, these get totally ignored.
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.
So can this go away now?
|
||
def _is_group(parser): | ||
return getattr(parser, '_subparsers', None) is not None \ | ||
or getattr(parser, 'choices', None) is not None |
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.
It would be good to connect with the VSCode guys as well. They need some public functions like this to insulate their extension form implementation changes within the CLI that cause their stuff to break.
@@ -197,7 +197,7 @@ | |||
""" | |||
|
|||
helps['vm availability-set delete'] = """ | |||
type: command' | |||
type: command |
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.
Look at the linter already doing its job!!!
create_invoker_and_load_cmds_and_args(az_cli) | ||
loaded_help = get_all_help(az_cli) | ||
command_table = az_cli.invocation.commands_loader.command_table | ||
add_id_parameters(None, cmd_tbl=command_table) |
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.
Yes, it should...
I mean, it is really loading the commands and args if it leaves args out?
@command_group_rule('Checking missing help for command-groups...') | ||
def missing_group_help_rule(linter, command_group_name): | ||
if not linter.get_command_group_help(command_group_name): | ||
print('--Command-Group: `%s`- Missing help.' % command_group_name) |
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.
I think you need to distinguish between a linter violation, exception, and warning. For example, the conditional logic in the callable should concern itself only with whether a linter violation occurred (i.e. is this the situation I was designed to detect?). If so, the rule should have metadata of some kind to say whether a violation should be interpreted as an Error (exit code 1) or a Warning (exit code 0)
{ | ||
"help_entries": { | ||
"example_entry": [ | ||
"unrecognized_help_entry_rule" |
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.
This piece should be vetted through the whole team, but I see some of your points.
I would also add that JSON is a pretty unfriendly format and would recommend something else (YAML, config format, etc.)
77b0cca
to
7ad9324
Compare
linter manager, some feedback
--modules, yml exclusions transferred to __init__.py
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.
My only request is to remove the "allow_failures" from the CI and limit the run to those tests that are passing.
.travis.yml
Outdated
@@ -55,3 +59,5 @@ jobs: | |||
python: 3.6 | |||
env: PURPOSE='Automation Docker' | |||
if: repo = Azure/azure-cli and type = push | |||
allow_failures: | |||
- env: PURPOSE='Lint Command Table and Help' |
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.
So can this go away now?
rule_exclusions: | ||
- missing_parameter_help_rule | ||
|
||
... |
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.
Like the new format!
What's the ... for at the end?
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.
Indicates the end of a file.
It's optional, similar to the ---
at the start.
--- | ||
# this is an example linter exclusion file | ||
|
||
help-entry-name: |
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.
Consider adding a comment of an example.
help-entry-name
still doesn't tell me what to actually type.
Would it be what is in helps['this text']
as this text
?
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.
yep, that's exactly it
Addresses: #734
(I'll make a new issue with just the rules list and close the issue above once this is merged)
-added doc help gen and loading of all commands to core util
-command linter rules can be one of: 'parameter_rule', 'command_rule', 'command_group_rule', 'help_file_entry_rule'