GithubHelp home page GithubHelp logo

instagram / fixit Goto Github PK

View Code? Open in Web Editor NEW
648.0 648.0 60.0 821 KB

Advanced Python linting framework with auto-fixes and hierarchical configuration that makes it easy to write custom in-repo lint rules.

Home Page: https://fixit.rtfd.io/en/latest/

License: Other

Python 99.81% Makefile 0.19%

fixit's People

Contributors

7imbrook avatar acharles7 avatar amyreese avatar arembridge avatar dependabot[bot] avatar duzumaki avatar haroldopsf avatar hauntsaninja avatar isidentical avatar jiayiyaij avatar jimmylai avatar josieesh avatar liangming168 avatar lisroach avatar llllvvuu avatar lpetre avatar luciawlli avatar max-muoto avatar michel-slm avatar mkniewallner avatar mvismonte avatar nt591 avatar pwoolvett avatar samueljsb avatar thatch avatar tsx avatar ulgens avatar yajus114 avatar yoonphillip avatar zsol avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fixit's Issues

Expose fixit as console script

Otherwise you can't install it via pipx.

pipx install fixit                                                                                                                                                                                                                           Note: Dependent package 'flake8>=3.8.1' contains 1 apps
  - flake8
Note: Dependent package 'pyflakes<2.3.0,>=2.2.0' contains 1 apps
  - pyflakes
Note: Dependent package 'pycodestyle<2.7.0,>=2.6.0a1' contains 1 apps
  - pycodestyle

No apps associated with package fixit. Try again with '--include-deps' to include apps of dependent packages, which are listed above. If you are attempting to install a library, pipx should not be used. Consider using pip or a similar tool instead.

apply_fix is too slow

Currently apply_fix is too slow. I reckon this is due to the fact that if a file has many violations, the linter will be run as many times. For LibCST it too several minutes to just fix one of the affected files.

The code says:

Applying a single fix at a time prevents the scenario where multiple autofixes combine in a way that results in invalid code.

What do you think of applying all the fixes with disjoint line ranges?

Show a better warning for missing pyre dependency

When running fixit I encountered the following error:

python3 -m fixit.cli.run_rules
Scanning 1 files
Testing 21 rules

Encountered exception <class 'Exception'> for the following paths:
./main.py
Running `pyre start` may solve the issue.
main.py:4:1
    E305: expected 2 blank lines after class or function definition, found 1

Found 1 reports in 1 files in 0.33 seconds.

My example project has a single python file, main.py, with the following content:

def main() -> None:
    print("hello")

if __name__ == '__main__':
    main()

Here's the repository for reference: https://github.com/chdsbd/fixit-example

Unrelated, but I was surprised to see messages about pyre and flake8. Can fixit be run without these?

Update imports with autofix

Hi, I'm looking into creating a rule telling users not to use a certain base class, but rather use a different one. Providing an autofix for this would require changing an import, which I can't figure out how to do. Is this currently possible?

For example I'd like to replace this:

from a import A

class MyClass(A):
    pass

with this:

from b import B

class MyClass(B):
    pass

I see that the NoNamedTupleRule leaves old imports in place and does not add an import for dataclasses, so I'm half assuming this isn't possible at the moment. Any plans to add support for this? Let me know if there's anything I can do to help :)

Passing no providers to FullRepoMetadataConfig causes exception

My ultimate goal is to use Fixit without Pyre, and without having to pass timeout=0 to the TypeInferenceProvider to force a breakage in the pyre subprocess call.

Created a simple repro:

from pathlib import Path
import logging
import sys
import libcst as cst
from fixit._version import FIXIT_VERSION
from fixit.common.config import get_lint_config
from fixit.common.config import get_rules_from_config
from fixit.common.full_repo_metadata import FullRepoMetadataConfig, get_repo_caches
from fixit.rule_lint_engine import lint_file
from libcst.metadata import MetadataWrapper, TypeInferenceProvider


def main(filename):
    full_repo_metadata_config = FullRepoMetadataConfig(
        providers=set(),  # <--- passing in no provider here instead of TypeInferenceProvider
        timeout_seconds=120,  # <--- If I were passing TypeInferenceProvider I would set this to 0
        logger=logging.getLogger("Metadata"),
    )

    with open(filename, "rb") as f:
        source = f.read()
    metadata_caches = get_repo_caches([filename], full_repo_metadata_config)
    try:
        cst_wrapper = MetadataWrapper(
            cst.parse_module(source),
            True,
            metadata_caches[filename],
        )
    except Exception:
        cst_wrapper = None
    rules = get_rules_from_config()
    config = get_lint_config()
    results = lint_file(
        Path(filename),
        source,
        rules=rules,
        config=config,
        cst_wrapper=cst_wrapper,
        find_unused_suppressions=True,
    )
    print(results)

if __name__ == "__main__":
    filename = sys.argv[0]
    main(filename)

Running this file results in:

(fixit-env) lisroach@lisroach-mbp Fixit % python3 hey.py hey.py
Traceback (most recent call last):
  File "hey.py", line 45, in <module>
    main(filename)
  File "hey.py", line 39, in main
    find_unused_suppressions=True,
  File "/Users/lisroach/Fixit/fixit/rule_lint_engine.py", line 115, in lint_file
    _visit_cst_rules_with_context(cst_wrapper, cst_rules, cst_context)
  File "/Users/lisroach/Fixit/fixit/rule_lint_engine.py", line 50, in _visit_cst_rules_with_context
    rule_instances, before_visit=before_visit, after_leave=after_leave
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/metadata/wrapper.py", line 222, in visit_batched
    stack.enter_context(v.resolve(self))
  File "/opt/homebrew/Cellar/python37/3.7.5_3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/contextlib.py", line 427, in enter_context
    result = _cm_type.__enter__(cm)
  File "/opt/homebrew/Cellar/python37/3.7.5_3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/contextlib.py", line 112, in __enter__
    return next(self.gen)
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/_metadata_dependent.py", line 85, in resolve
    self.metadata = wrapper.resolve_many(self.get_inherited_dependencies())
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/metadata/wrapper.py", line 196, in resolve_many
    _resolve_impl(self, providers)
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/metadata/wrapper.py", line 97, in _resolve_impl
    p(wrapper._cache.get(p)) if p.gen_cache else p() for p in batchable
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/metadata/wrapper.py", line 97, in <listcomp>
    p(wrapper._cache.get(p)) if p.gen_cache else p() for p in batchable
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/metadata/type_inference_provider.py", line 75, in __init__
    super().__init__(cache)
  File "/Users/lisroach/fixit-env/lib/python3.7/site-packages/libcst/metadata/base_provider.py", line 69, in __init__
    f"Cache is required for initializing {self.__class__.__name__}."

Integrate Fixit into a VSCode plugin

Integrate Fixit into a VSCode plugin (and potentially any other IDE), so developer can easily install it and use it on VSCode.

Fixit can be run automatically when developers save source file updates and show warning inline with wavy underline or lightbulb UIs.

Then developers can click on the UI or press some shortcut to apply autofix.

Some helper is provided to make this easy to implement, e.g. lint_file returns a list of violation reports (BaseLintRuleReport) and provides patch property when autofix is available.

https://github.com/Instagram/Fixit/blob/master/fixit/rule_lint_engine.py#L54

Script to add new rules

I think it would be a great idea to add a script to generate a higher-level skeleton of adding a new rule in fixit/rules
The possible skeleton would be

# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

import libcst as cst

from fixit import CstLintRule, InvalidTestCase as Invalid, ValidTestCase as Valid

class NewRule(CstLintRule):
    '''
    docstring or new_rule information
    '''
    MESSAGE = 'message'
    VALID = [Valid()]
    INVALID = [Invalid()]

Right now, I have only added license, imports, and class. But, we can add other things too if needed in future.
if we generate this skeleton using python scripts then It will be very easy for other contributors to add new rules.

So, to add new rule, run add_new_rule.py file (will be located in fixit/scripts) using
python -m fixit.scripts.add_new_rule --name rule_name
This will add new_rule.py in fixit/rules

Please let me know what you think.
I am happy to make PR for this script in fixit package.

Rules can provide autofix and open source

  • UseAttributeAccessRule
  • UseAttributeValueAssignRule
  • NoInheritFromNamedTuple
  • NoUnnecessaryFormatString
  • SortedAttributesRule
  • UnwrapStringTypeAnnotationRule
  • MissingAnnotationLintRule
  • UseFstringRule
  • DoNotShadowPackagesRule
  • DataclassExplicitFrozenRule
  • UseAssertInRule

[4] replace noqa comment with specific lint rule name comments

noqa is not ideal because it silence all lint suggestions and can accidentally silence line unintentional errors.
We want to be specific and just use lint-fixme or lint-ignore.
We'd like to build a script to replace noqa comment as line-fixme or lint-ignore comments.
After that, we'll remove the support of noqa in

# Legacy inline `# noqa` comments. This matches flake8's behavior.
# Process these after `# lint-ignore` comments, because in the case of duplicate
# or overlapping ignores, we'd prefer to mark the noqa as unused, instead of the
# more modern `# lint-ignore` comment.
for tok in comment_info.comments:
match = NOQA_INLINE_REGEXP.search(tok.string)
if match:
normalized_line = line_mapping_info.physical_to_logical[tok.start[0]]
codes = _parse_comma_separated_rules(match.group("codes"))
comment = SuppressionComment(codes, [tok])
local_suppression_comments.append(comment)
local_suppression_comments_by_line[normalized_line].append(comment)

Incorrect fix for "%s" % "\n"

Please see example below:

echo '"%s" % "\n"' > test.py

python -m  fixit.cli.apply_fix test.py

Scanning 1 files
test.py

Encountered exception <class 'Exception'> for the following paths:
test.py
Running `pyre start` may solve the issue.
test.py:1:1 [applied fix]
    UseFstringRule: Do not use printf style formatting or .format(). Use f-string instead to be more
    readable and efficient. See https://fburl.com/usefstring.
Encountered exception <class 'Exception'> for the following paths:
test.py
test.py
test.py
Running `pyre start` may solve the issue.
All done! โœจ ๐Ÿฐ โœจ
1 file left unchanged.

Found 1 reports in 1 files in 0.33 seconds.


cat test.py 
f"{'\n'}"


python test.py 
  File "test.py", line 1
    f"{'\n'}"
    ^
SyntaxError: f-string expression part cannot include a backslash

Discussion: Fixit Config File Requirements

When scaling Fixit to large monolithic repos we have run into challenges with the config file. In monorepos it is important to be able to do directory level configurations, that way users can tweak rules for just their projects without affecting the entire repository.

I wanted to kick off a discussion around this problem first by getting an agreement on the requirements for fixitโ€™s configs. Here is the problem space I wish to address:

Problem: I want to turn on (or off) a lint rule just for my directory

In Fixit today there is only one top-level config, and only one way to turn off rules for the repo: block_list_rules.
This means we can turn off rules for everyone or no one, with no subdirectory level support.

Problem: I want to customize a rule_config just for my directory

Same as the issue above, since there is only one root-level config file there is no way to customize rules for subdirectories.

Proposals

Inheritance of config files

The problems listed can be mostly addressed by supporting subdirectories with overriding configs. This way a top-level config file can exist that contains the defaults the majority of the repo will utilize, but subdirectories can override these defaults with their configurations.

Based on our experiences with Flake8, I propose these files support inheritance. When there is no config inheritance, users usually make copies of the top-level config to keep most of the defaults and only change the few they need. Unfortunately, these copies are not linked with the top-level config and get out of sync very quickly. Worse still, many users assume inheritance is a property already of the config files and do not copy the top-level configs at all, losing valuable and sometimes necessary customizations.

Enabling inheritance of config files will enable custom configurations per-directory, while keeping in sync with the top-level config and its defaults.

Inheritance opens up a lot of questions, which I have listed in the next section below.

Create an allow_list_rules key

To fully address the first problem I propose we add a new key, allow_list_rules.
Adding an allow_list_rules section to the config will enable us to mark rules as โ€˜onโ€™, instead of defaulting to everything running. Using this, directories can turn on rules they want without the rule also being turned on everywhere else.

This will also make it less risky to upgrade to a new version of Fixit. New rules can be added to Fixit, but they wonโ€™t start firing until we explicitly turn them on. This gives us a chance to test and vet new rules.

block_list_rules should take precedence. This way inherited rules that are on can be overridden and turned off, as well as it enables the root config to turn off dangerous rules for all leaf configs in a single location.

Inheritance Questions

Assuming people are on board with the idea of inheritance, there are several questions that should be discussed.

Should leaf configs override the root, or merge with the root config?

Example:
root/.fixit.config.yaml

block_list_rules:
- BlockListedRule

root/subdir/.fixit.config.yaml

block_list_rules:
- OtherBlockListedRule

Overridden the final file looks like...

block_list_rules:
- OtherBlockListedRule

Merging/appending instead of overriding becomes...

block_list_rules:
- OtherBlockListedRule
- BlockListedRule

I think the answer here is merging should be the default, at least for keys like block_list_rules. This way the root config has the power to en-mass turn off rules, which could be valuable if a rule is found to be dangerous.

Letโ€™s check again with a more complicated example showing a deep merge in the rule_config.

root/.fixit.config.yaml

block_list_patterns:
- '@generated'
- '@nolint'
block_list_rules:
- BlockListedRule
fixture_dir: ./tests/fixtures
formatter:
- black
- '-'
packages:
- fixit.rules
repo_root: .
rule_config:
    ImportConstraintsRule:
        fixit:
            rules: [["*", "allow"]]

root/subdir/.fixit.config.yaml

block_list_rules:
- OtherBlockListedRule
packages:
- myproject.rules
rule_config:
    ImportConstraintsRule:
        mysubdir:
            rules: [
                ["fixit", "allow"],
                ["other_module", "deny"],
                ["*", "deny"]
            ]
            ignore_tests: True
            ignore_types: True
            message: "'{imported}' cannot be imported from within '{current_file}'."

Merged config

block_list_patterns:
- '@generated'
- '@nolint'
block_list_rules:
- BlockListedRule
- OtherBlockListedRule
fixture_dir: ./tests/fixtures
formatter:
- black
- '-'
packages:
- fixit.rules
- myproject.rules
repo_root: .
rule_config:
  ImportConstraintsRule:
    fixit:
    - - '*'
      - allow
    mysubdir:
      rules:
      - - fixit
        - allow
      - - other_module
        - deny
      - - '*'
        - deny
      ignore_tests: true
      ignore_types: true
      message: '''{imported}'' cannot be imported from within ''{current_file}''.'

This again looks right to me. Weโ€™ve added some custom subdir configurations to the ImportConstraintsRule, and weโ€™ve extended the packages and block_list_rules.

Should leaf configs always deep merge with the root config?

Assuming people agree with my assessment that the above config looks good, does it make sense for it to always deep merge? Letโ€™s look at an example where keys in both the root and the leaf are identical:

root/.fixit.config.yaml

rule_config:
    ImportConstraintsRule:
        fixit:
            rules: [["*", "allow"]]

root/subdir/.fixit.config.yaml

rule_config:
    ImportConstraintsRule:
        fixit:
            rules: [
                ["fixit", "allow"],
                ["other_module", "deny"],
                ["*", "deny"]
            ]
            ignore_tests: True
            ignore_types: True
            message: "'{imported}' cannot be imported from within '{current_file}'."

Merged file

rule_config:
  ImportConstraintsRule:
    fixit:
      rules:
      - - '*'
        - allow
      - - fixit
        - allow
      - - other_module
        - deny
      - - '*'
        - deny
      ignore_tests: true
      ignore_types: true
      message: '''{imported}'' cannot be imported from within ''{current_file}''.'

The rule_configโ€™s rules section has been appended, but this results in an invalid rule: there are two * rules which will raise an exception. Here the deep merge technique failed.

What if we override duplicate keys in the rule_config section, and merge the rest?

This is possible to do. Overriding the rule_config keys and merging everywhere else would result in:

block_list_patterns:
- '@generated'
- '@nolint'
block_list_rules:
- BlockListedRule
- OtherBlockListedRule
fixture_dir: ./tests/fixtures
formatter:
- black
- '-'
packages:
- fixit.rules
- myproject.rules
repo_root: .
rule_config:
    ImportConstraintsRule:
        fixit:
            rules: [
                ["fixit", "allow"],
                ["other_module", "deny"],
                ["*", "deny"]
            ]
            ignore_tests: True
            ignore_types: True
            message: "'{imported}' cannot be imported from within '{current_file}'."

Which is a correct config file. This is not prohibitively complicated to implement, we can choose which keys we want to be merged and which we want to be overridden with a tool like jsonmerge.

This strategy works well for the few rules I have tested it out on. My only concern is the variety of rules. I wonder if some rules would be better configured with a deep merge, instead of overriding. Iโ€™d appreciate any thoughts around this. It is possible to require each rule author to define whether they want their rule configured via a deep merge or overriding, but that is adding a layer of complexity.

Config resolution order?

How do we want to define the order of config inheritance? Thereโ€™s several questions here:

  • Should configs be limited to inheriting only from the root config?
  • Should configs be able to inherit from configs outside of their directory tree?
    • If so, how does the user define this (a new key in the config?)
    • How do we avoid the diamond problem and determine order?

I think the most appealing way is to inherit via the directory hierarchy. We can read yaml files starting from the root (where default values would be) to the leaf (where most specific values would be, which should take precedence).
Like so:

root
โ”œโ”€โ”€root_config.yaml
โ””โ”€โ”€myproject
    โ”œโ”€โ”€sub_config.yaml
    โ””โ”€โ”€subdir
        โ””โ”€โ”€ leaf_config.yaml

Here leaf_config.yaml inherits from sub_config.yaml and root_config.yaml. sub_config.yaml inherits just from root_config.yaml. The user isnโ€™t be required to do any extra configurations to indicate from where they wish to inherit, it happens automatically. I think this is intuitive to users, and keeps the implementation and maintenance simple.

Embedding partial configs?

This idea adds some complexity, but it is worth discussing. We could use interpolation or includes to embed whole configs in parts of another config. This might be interesting for individual rules in the rule_config section.

Instead of defining default rule customizations in the root config, rule authors could generate a config just for their rule, and embed that into the root config. Something like this:

rule_config: !include root/rules/**/*.yaml

That would embed all the configs defined under rules.

This adds the overhead that rule authors would need to determine โ€œdefaultโ€ rule configurations, and would result in the creation of more config files. You also would not be able to see the configuration of all the rules at a glance. But it would keep the root config a lot smaller and more manageable, and make it relatively easy to find and update rule_configs specific to individual rules.

Thanks for reading this far! This is a brain dump of my thoughts, please comment on anything you disagree (or agree) with, or anything I have not thought of.

Incorrect fix for value == "variables:{}".format(name)'

Example:

echo 'value == "variables:{}".format(name)' > test.py

python -m  fixit.cli.apply_fix test.py

Scanning 1 files
test.py

Encountered exception <class 'Exception'> for the following paths:
test.py
Running `pyre start` may solve the issue.
test.py:1:1 [applied fix]
    CompareSingletonPrimitivesByIsRule: Comparisons to singleton primitives should not be done with
    == or !=, as they check equality rather than identiy. Use `is` or `is not` instead.
Encountered exception <class 'Exception'> for the following paths:
test.py
test.py
test.py
Running `pyre start` may solve the issue.
test.py:1:10
    UseFstringRule: Do not use printf style formatting or .format(). Use f-string instead to be more
    readable and efficient. See https://fburl.com/usefstring.
All done! โœจ ๐Ÿฐ โœจ
1 file left unchanged.

Found 2 reports in 1 files in 0.34 seconds.

cat test.py

value is "variables:{}".format(name)

Improve cli interface

  • Add a way to disable the RemoveUnusedSuppressionsRule
  • Standardize the flag for enabling disabling ignore comments. apply_fix uses get_skip_ignore_comments_parser, but run_rules uses get_use_ignore_comments_parser, which has True as a default, meaning it's impossible to disable it.

[3] Remove numeric code and convert lint suppression comment for all rules

This is to migrate all rules to use class name instead of numeric code.
We need to make sure the lint rule has proper name.
The class name should be short and actionable. Each rule class should ends with Rule (not LintRule).
Instead of describe the issue, describe the action needs to take to fixe it is more actionable.
E.g. AwaitInLoopLintRule โ†’ GatherSequentialAwaitRule
NotUsingFstringRule โ†’ UseFstringRule

If the action need is to remove/cleanup something, it can be named as NoSomethingRule.

Pyre failures with LibCST 0.3.19

fixit/common/generate_pyre_fixtures.py:54:50 Incompatible parameter type [6]: Expected `typing.List[str]` for 1st anonymous parameter to call `run_command` but got `str`.
fixit/common/generate_pyre_fixtures.py:75:50 Incompatible parameter type [6]: Expected `typing.List[str]` for 1st anonymous parameter to call `run_command` but got `str`.
fixit/common/generate_pyre_fixtures.py:89:24 Incompatible parameter type [6]: Expected `typing.List[str]` for 1st anonymous parameter to call `run_command` but got `str`.
fixit/rules/cls_in_classmethod.py:283:20 Incompatible parameter type [6]: Expected `typing.Iterable[Union[cst._nodes.expression.Attribute, cst._nodes.expression.Name]]` for 1st anonymous parameter to call `list.__iadd__` but got `List[Union[cst._nodes.expression.Attribute, cst._nodes.expression.BaseString, cst._nodes.expression.Name]]`.

Feature Request: configuration using `pyproject.toml`

Would you accept a PR to configure fixit using pyproject.toml?

something like this:

diff --git a/fixit/common/config.py b/fixit/common/config.py
index 9f723e0..e361f6a 100644
--- a/fixit/common/config.py
+++ b/fixit/common/config.py
@@ -9,16 +9,19 @@ import re
 from dataclasses import asdict
 from functools import lru_cache
 from pathlib import Path
-from typing import Any, Dict, Optional, Pattern, Set
+from typing import Any, Dict, List, Pattern, Set
 
 import yaml
+import toml
 
 from fixit.common.base import LintConfig
 from fixit.common.utils import LintRuleCollectionT, import_distinct_rules_from_package
 
 
-LINT_CONFIG_FILE_NAME: Path = Path(".fixit.config.yaml")
-
+LINT_CONFIG_FILE_NAMES: List[str] = [
+    ".fixit.config.yaml",
+    "pyproject.toml",
+]
 # https://gitlab.com/pycqa/flake8/blob/9631dac52aa6ed8a3de9d0983c/src/flake8/defaults.py
 NOQA_INLINE_REGEXP: Pattern[str] = re.compile(
     # TODO: Deprecate
@@ -115,17 +118,21 @@ def get_validated_settings(
 def get_lint_config() -> LintConfig:
     config = {}
 
-    cwd = Path.cwd()
-    for directory in (cwd, *cwd.parents):
-        # Check for config file.
-        possible_config = directory / LINT_CONFIG_FILE_NAME
-        if possible_config.is_file():
-            with open(possible_config, "r") as f:
-                file_content = yaml.safe_load(f.read())
-
-            if isinstance(file_content, dict):
-                config = get_validated_settings(file_content, directory)
-                break
+    current_dir = Path.cwd()
+    for directory in (current_dir, *current_dir.parents):
+        for lint_config_file_name in LINT_CONFIG_FILE_NAMES:
+            possible_config = directory / lint_config_file_name
+            if possible_config.is_file():
+                with open(possible_config, "r") as f:
+                    raw = f.read()
+                file_content = {
+                    ".yaml": yaml.safe_load,
+                    ".toml": lambda text: toml.loads(text)["tool.fixit"],
+                }[possible_config.suffix](raw)
+
+                if isinstance(file_content, dict):
+                    config = get_validated_settings(file_content, directory)
+                    break
 
     # Find formatter executable if there is one.
     formatter_args = config.get("formatter", DEFAULT_FORMATTER)
@@ -139,7 +146,7 @@ def get_lint_config() -> LintConfig:
 
 def gen_config_file() -> None:
     # Generates a `.fixit.config.yaml` file with defaults in the current working dir.
-    config_file = LINT_CONFIG_FILE_NAME.resolve()
+    config_file = Path(LINT_CONFIG_FILE_NAMES[0]).resolve()
     default_config_dict = asdict(LintConfig())
     with open(config_file, "w") as cf:
         yaml.dump(default_config_dict, cf)

Change should_skip_file to not require the MetadataWrapper

Today should_skip_file is an instance method and hence requires the metadata wrapper, which in turn requires to parse the code.

However, the two rules defining this method, only use the file name and the config.

It would be ideal if we could change it so that the check could be performed without having to parse the code.

For example to speed up the linting I want to just use the _source (I know it's private now) checking if it matches a very simplified regex (note: this regex would be autogenerated).

Simplify import module for commonly used helpers.

CstLintRule, CstContext, LintConfig, CstLintReport, lint_file is current in fixit.common.base.
Valid Invalid

We can move or export common helpers to a top level module to make it easier to use.
E.g. developer can just do
from fixit import CstLintRule or from fixit.common import CstLintRule,
instead of from fixit.common.base import CstLintRule

Allow files to be excluded for AddMissingHeaderRule

Add an option to AddMissingHeaderRule so some files can be exempt from it.

Something like:

rule_config:
   AddMissingHeaderRule:
       path: pkg/*.py
       exclude:
           - pkg/uninteresting*.py
       header: |-
           # header line 1
           # header line 2

Add fixit's CLI to documentation

Add Fixit's CLI commands to documentation (use #148 as reference)

  • fixit's subcommands
$ fixit run_rules --help
$ fixit apply_fix --help
$ fixit insert_suppressions --help
$ fixit add_new_rule --help

The above sub-commands and its usage description will be added to README.rst and getting_started.ipynb

Docs to describe updating the docs

I found myself unsure of how to update the docs other than regenerating them with tox -e docs

We should update the README to explain the steps:

  1. activate the venv
  2. Run jupyter notebook
  3. This will launch a browser window where you can edit the docs directly
  4. Use "save and checkpoint" -> I think?
  5. Regenerate the docs with tox -e docs

Fixit CLI

Launch a brand new fixit-cli using argparse (Will fix #124)

Currently, all the scripts in fixit/cli are individual, which is good as per my plan of new CLI.

Here's my plan of implementing this:
The CLI will look like the following:

setup.py

entry_points={
        'console_scripts': [
            'fixit-cli = cli.main:launch_cli'
        ],
    },

fixit/cli/main.py

from fixit.cli import apply_fix

def _parse_flags(argv: List[str]) -> argparse.Namespace:
    parser = argparse_flags.ArgumentParser(prog="fixit", description="Fixit CLI ")
    # Register sub-commands
    subparser = parser.add_subparsers(title='command')

    # Will add all individual scripts i.e.
    add_new_rule.register_subparser(subparser) # add_new_rule
    apply_fix.register_subparser(subparser) # apply_fix
    ...
    return parser.parse_args(argv[1:])

def main(args: argparse.Namespace) -> None:
    args.subparser_fn(args)

def launch_cli() -> None:
    app.run(main, flags_parser=_parse_flags)

if __name__ == "__main__":
    launch_cli()

fixit/cli/apply_fix.py

def register_subparser(parsers: argparse._SubParsersAction) -> None:
    """Add subparser for `apply_fix` command."""
    apply_fix_parser = parsers.add_parser(
        "apply_fix",
        help=APPLY_FIX_HELP_TEXT,
        parents=[get_paths_parser(), ...]
    )

    apply_fix_parser.set_defaults(subparser_fn=_apply_fixes)


def _apply_fixes(args: argparse.Namespace):
    # to-do

By doing this we can use fixit as a console script, which is very convenient.

I am planning to do this in small and individual PRs, starting from fixit/cli/main.py then each sub-commands (apply_fix, add_new_rule), etc.
Other files will stay as it is(utils.py, args.py).

Let me know if you want to add anything or have any suggestions on this. So, I can start ASAP

Note: we can change CLI name later on or suggest the best name that suits well for this

@jimmylai

Fixit cli not working with python 3.7.0

When running python -m fixit.cli.run_rules --help gives

@dataclass(frozen=True, eq=False, unsafe_hash=False)
TypeError: dataclass() got an unexpected keyword argument 'unsafe_hash'

In Python 3.7.0 there is no unsafe_hash parameter of data classes
So, In Fixit module, we have to explicitly specify that this package work with python >= 3.7.8 because the implementation of unsafe_hash available in Python >= 3.7.8

or removing unsafe_hash=False will remove this error. In such case, We have to figure out a way to replace unsafe_hash

Error in Readme of the description of setting up virtualenv

As stated in the title, there is an error in the Readme in the setting up vr here:
pip install -r requirements.txt -r requirements-dev.txt

In one of the commands above, there is a command cd fixit. Because of this, the paths of the requirement files are incorrect. The above command should be:
pip install -r ../requirements.txt -r ../requirements-dev.txt

[5] build a lint rule to detect unused lint suppression comments

As a follow up after we migrated all existing lint comments to use class name, we need a lint rule to detect unused lint suppression comments and provide autofix to remove the comments automatically.
So the codebase won't be overwhelmed by not used comments since they may be misleading.

[2] Update _get_code logic to using classname as code

The _get_code currently requires using IG\d+ as prefix in the message.

def _get_code(message: str) -> str:
"""Extract the lint code from the beginning of the lint message."""
# TODO: This shouldn't really exist, and we should treat lint codes and messages as
# separate concepts.
code_match = re.match(r"^(?P<code>IG\d+) \S", message)
if not code_match:
raise ValueError(
"Report messages should begin with IGXX, where XX is the number "
+ "associated with the rule, followed by a single space."
)
return code_match.group("code")

We'd like to update it to support the iterative migration:

  1. Start with a lint rules and then removes its numeric code from message.
  2. Use the new tool to convert their silence comment as class name.

Once we did that for all rules, we can remove the logic of numeric code completely from _get_code.

Incorrect rewriting of dict(test=[])

Example interaction:

echo "dict(test=[])" > test.py
python -m  fixit.cli.apply_fix test.py 
Scanning 1 files
test.py

Encountered exception <class 'Exception'> for the following paths:
test.py
Running `pyre start` may solve the issue.
test.py:1:1 [applied fix]
    RewriteToLiteralRule: It's unnecessary to use a list or tuple within a call to dict sincethere is
    literal syntax for this type
Encountered exception <class 'Exception'> for the following paths:
test.py
test.py
test.py
Running `pyre start` may solve the issue.
All done! โœจ ๐Ÿฐ โœจ
1 file left unchanged.

Found 1 reports in 1 files in 0.33 seconds.
(py3_venv) wolf@ldp-wolf:~/ros_ws_py3/src/real_world$ cat test.py
{}
(py3_venv) wolf@ldp-wolf:~/ros_ws_py3/src/real_world$ 

RewriteToLiteralRule is losing a field in the dictionary.

Use of old-style format leads to advice with broken link

Hey,

Thanks for the tool.

There appears to be an invalid link in (at least) one of the advices.

Note the attached file.

When running python -m fixit.cli.run_rules with a file with the following contents:

myname = "richie"
string_old = "This is {name}'s string".format(name=myname)

string_new = f"This is {myname}'s string"

I get the following advice:

invalid_link_suggestion.py:2:14
UseFstringRule: Do not use printf style formatting or .format(). Use f-string instead to be more readable and
efficient. See https://fburl.com/usefstring.```

I mean, I like fburls, I remember them well. However, I (and pretty much every non-FB employee) have no access to them.

It would probably be better if this pointed to a link accessible from the public web.

Linter rule implementation. Help wanted.

First of all โ€“ thank you so much for libCST and Fixit and making them both publicly available.

I'm currently trying to migrate my existing Pylint rules to Fixit and I'm stuck on one of them. If you can provide some help on how it can be implemented on libCST and Fixit โ€“ I would really appreciate it.

I want to make sure that builtin next() function is always called with an explicitly specified default value OR wrapped around with try / except and StopIteration exception is handled.

Invalid case:

def hello(things):
    next(thing for thing in things)

Valid cases:

def hello(things):
     next(thing for thing in things, "howdy")
...

def hello(things):
   try:
       return next((thing for thing in things))
   except StopIteration:
       return "hi"

My existing Pylint rule relies on node_ignores_exception helper.

p.s. If it's not an appropriate place to ask questions like that โ€“ please let me know where should I do that instead. Thanks!

Pyre Failing on master branch

Pyre failed due to a type error was found in fixit/rules/cls_in_classmethod.py:286:26 and that's related to the new LibCST release.
https://github.com/Instagram/Fixit/runs/2271833508
The same issue happens on master branch and it's better to fix it before merging new PRs.
FlattenSentinel is added in LibCST 0.3.18 and we need to update the type annotation of report().

replacement: Optional[Union[cst.CSTNode, cst.RemovalSentinel]] = None,

Originally posted by @jimmylai in #182 (comment)

Autofix needed for three files

Not sure what changed but seems like something under the hood is causing these three files to need an autofix:

fixit/cli/add_new_rule.py
fixit/common/unused_suppressions.py
fixit/common/utils.py

It's causing breakages on my unrelated PRs, so I am going to open a new PR just to fix them.

Silence corresponding Flake8 suggestion

Some rules are replacement of Flake8 rules, we should silence them automatically when the rule is enabled to avoid duplicated suggestions.

  • ComparePrimitivesByEqualRule replaces F632
  • CompareSingletonPrimitivesByIsRule replaces E711 and E712
  • NoStringTypeAnnotationRule replaces F821

Avoid linting venv dependencies by default

In README.md and the getting started doc, the doc suggested to execute python -m fixit.cli.run_rules to trigger the linter. However, this would trigger unwanted behavior if fixit is installed inside venv.

To reproduce:

mkdir foo
cd foo
python -m venv venv
. venv/bin/activate 
pip install fixit
python -m fixit.cli.run_rules

fixit would scan all python (dependency) source codes inside venv directory, which may not be the intended behavior for the users.

Scanning 856 files                                                                                                                                                                                                  
Testing 21 rules                                                                                          
                                                                                                                                                                                                                    
Encountered exception <class 'Exception'> for the following paths:                                        
./app.py                                                                                                                                                                                                            
./venv/lib/python3.8/site-packages/mccabe.py                                                                                                                                                                        
./venv/lib/python3.8/site-packages/pycodestyle.py                                                                                                                                                                   
./venv/lib/python3.8/site-packages/typing_inspect.py                                                      
./venv/lib/python3.8/site-packages/typing_extensions.py                                                                                                                                                             
...

Following the docs, it suggested to generate the init config if one would like to specify the repo root. However, the default init config would also yield an exception since the default value of formatter is an empty string.

python -m fixit.cli.init_config
python -m fixit.cli.run_rules

...
  File "/tmp/foo/venv/lib/python3.8/site-packages/fixit/common/config.py", line 137, in get_lint_config
    exe = distutils.spawn.find_executable(formatter_args[0]) or formatter_args[0]
IndexError: list index out of range
cat .fixit.config.yaml 
block_list_patterns:
- '@generated'
- '@nolint'
block_list_rules: []
fixture_dir: ./fixtures
formatter: []                    <----------------- formatter_args[0] doesn't exist
packages:
- fixit.rules
repo_root: .
rule_config: {}

Rules miss docstring

AvoidOrInExceptRule
AwaitAsyncCallRule
ComparePrimitivesByEqualRule
GatherSequentialAwaitRule
NoAssertEqualsRule
NoStaticIfConditionRule
...

Handle missing packages

If there are not packages in the config file (or no rules in the packages) we should surface a warning to the user that nothing is going to be run by Fixit.

Tutorials

  • use fixit
  • build a lint rule
  • test lint rules
  • handle existing violations
  • configuration

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.