Skip to content
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

Merged
merged 11 commits into from
Apr 5, 2018
Merged

Command linter #5952

merged 11 commits into from
Apr 5, 2018

Conversation

williexu
Copy link
Contributor

@williexu williexu commented Mar 29, 2018


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'

@williexu williexu requested review from derekbekoe and troydai March 29, 2018 19:39
@promptws
Copy link

View a preview at https://prompt.ws/r/Azure/azure-cli/5952
This is an experimental preview for @microsoft users.

@williexu williexu force-pushed the commandlint branch 5 times, most recently from 493e557 to eb1eb50 Compare March 29, 2018 20:58
Copy link
Member

@tjprescott tjprescott left a 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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert these changes.

Copy link
Contributor Author

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()
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why public?

Copy link
Contributor Author

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):
Copy link
Member

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):
Copy link
Member

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.

Copy link
Contributor Author

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"
Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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()):
Copy link
Member

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)

Copy link
Contributor Author

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)
Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.
Copy link
Member

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?

Copy link
Contributor Author

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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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))
Copy link
Member

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?

Copy link
Contributor Author

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)
Copy link
Member

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'
Copy link
Member

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.

Copy link
Member

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
Copy link
Member

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
Copy link
Member

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)
Copy link
Member

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)
Copy link
Member

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"
Copy link
Member

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.)

@williexu williexu force-pushed the commandlint branch 3 times, most recently from 77b0cca to 7ad9324 Compare April 4, 2018 18:38
Copy link
Member

@tjprescott tjprescott left a 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'
Copy link
Member

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

...
Copy link
Member

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?

Copy link
Contributor Author

@williexu williexu Apr 5, 2018

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:
Copy link
Member

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?

Copy link
Contributor Author

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

@tjprescott tjprescott merged commit 48a0c83 into Azure:dev Apr 5, 2018
@williexu williexu deleted the commandlint branch July 11, 2018 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants