pschanely / CrossHair

An analysis tool for Python that blurs the line between testing and type systems.
Other
1.03k stars 49 forks source link

NameError when running "crosshair check" #158

Closed barrywhart closed 2 years ago

barrywhart commented 2 years ago

Expected vs actual behavior Expected behavior: Successful run of crosshair check with useful output.

Actual behavior: crosshair check crashes with a runtime error, "NameError".

(sqlfluff-3.10.1) ➜  sqlfluff git:(main) ✗ crosshair check src/sqlfluff/rules
Traceback (most recent call last):
  File "/Users/bhart/.pyenv/versions/sqlfluff-3.10.1/bin/crosshair", line 8, in <module>
    sys.exit(main())
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/main.py", line 643, in main
    sys.exit(unwalled_main(cmd_args))
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/main.py", line 593, in unwalled_main
    return check(args, options, sys.stdout, sys.stderr)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/main.py", line 566, in check
    for message in run_checkables(analyze_any(entity, options)):
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 693, in run_checkables
    collector.extend(checkable.analyze())
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 672, in analyze
    for message in self.checkable.analyze():
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 624, in analyze
    analysis = analyze_calltree(options, conditions)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 918, in analyze_calltree
    call_analysis = attempt_call(
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 1110, in attempt_call
    bound_args = gen_args(conditions.sig) if bound_args is None else bound_args
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 571, in gen_args
    value = proxy_for_type(param.annotation, smt_name, allow_subtypes)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 534, in proxy_for_type
    return proxy_for_class(typ, varname)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 390, in proxy_for_class
    args = gen_args(constructor_sig)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 571, in gen_args
    value = proxy_for_type(param.annotation, smt_name, allow_subtypes)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 534, in proxy_for_type
    return proxy_for_class(typ, varname)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 390, in proxy_for_class
    args = gen_args(constructor_sig)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 571, in gen_args
    value = proxy_for_type(param.annotation, smt_name, allow_subtypes)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 531, in proxy_for_type
    return proxy_factory(recursive_proxy_factory, *type_args)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/libimpl/builtinslib.py", line 3533, in make_union_choice
    return creator(typ)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 452, in __call__
    return proxy_for_type(
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 534, in proxy_for_type
    return proxy_for_class(typ, varname)
  File "/Users/bhart/.pyenv/versions/3.10.1/envs/sqlfluff-3.10.1/lib/python3.10/site-packages/crosshair/core.py", line 373, in proxy_for_class
    data_members = get_type_hints(typ)
  File "/Users/bhart/.pyenv/versions/3.10.1/lib/python3.10/typing.py", line 1808, in get_type_hints
    value = _eval_type(value, base_globals, base_locals)
  File "/Users/bhart/.pyenv/versions/3.10.1/lib/python3.10/typing.py", line 326, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
  File "/Users/bhart/.pyenv/versions/3.10.1/lib/python3.10/typing.py", line 691, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
NameError: name 'TemplatedFile' is not defined

To Reproduce I'm trying to use Crosshair to detect bugs in the linting rules for SQLFluff, an open-source SQL linter/fixer/formatter written in Python. Unfortunately, it's a pretty sizable project, so the repro case involves cloning the full repo and running Crosscheck on a large portion of the code. If we're able to get this working and get useful output, it could be really helpful. The project is about 3 years old, and we are trying to stamp out bugs and reach a stable 1.0 release.

  1. Set up a local SQLFluff development environment. Docs here.
  2. In the file src/sqlfluff/core/rules/base.py, add a post: True postcondition to the BaseRule._eval() function.
  3. In the file src/sqlfluff/core/rules/loader.py, replace the glob.glob() call with hardcoded results. Example below.
  4. Run crosshair check src/sqlfluff/rules.
    for module in ['/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L001.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L002.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L003.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L004.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L005.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L006.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L007.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L008.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L009.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L010.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L011.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L012.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L013.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L014.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L015.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L016.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L017.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L018.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L019.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L020.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L021.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L022.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L023.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L024.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L025.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L026.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L027.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L028.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L029.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L030.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L031.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L032.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L033.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L034.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L035.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L036.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L037.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L038.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L039.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L040.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L041.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L042.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L043.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L044.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L045.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L046.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L047.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L048.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L049.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L050.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L051.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L052.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L053.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L054.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L055.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L056.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L057.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L058.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L059.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L060.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L061.py',
 '/Users/bhart/dev/sqlfluff/src/sqlfluff/rules/L062.py']:
pschanely commented 2 years ago

Thanks for the report! Looks like we're having trouble resolving some of the type annotations here. I'll investigate this week and get back to you.

barrywhart commented 2 years ago

Thanks! I've been really excited to try this project ever since I heard the podcast episode. In terms of Crosshair and SQLFluff, I'm hoping that the rules, each of which is fairly self contained, will be a good target for analysis.

pschanely commented 2 years ago

Ok! So, I spent some time digging into SQLFluff + CrossHair: it's an interesting use case! Most immediately, I've fixed the NameError issue at head. (essentially, the "TemplatedFile" import was guarded by an "if typing.TYPE_CHECKING:" condition; CrossHair has some ability to handle this, but it needed to be expanded to handle cases like this)

I'll cut a release later this week including the fix and update this issue then.

In the meantime, let me describe a little bit about what CrossHair does when analyzing BaseCondition._eval. CrossHair needs to supply a symbolic inputs to _eval, namely a RuleContext instance (and also self, which is a BaseRule). RuleContext is a sizable structure, with several instances of BaseSegment, which has many subclasses. CrossHair will recursively and nondeterministically pick subclasses to construct; it doesn't really create anything symbolic until it gets to a builtin type, like a string or integer. Because the classes you're attempting to construct are complex, you might consider limiting the amount of symbolic inputs you require. For instance, you probably don't want CrossHair to create a BaseRule via its constructor: instead, you might just check all the concrete rule instances directly; perhaps with a separate (unused) function that you just apply a contract to:

_RULES = Linter().get_ruleset()
def _check_no_exceptions_with_crosshair(segment: BaseSegment, raw_stack: List[RawSegment]):
    """ post: True """
    context = RuleContext(segment, (), (), (), raw_stack, None, dialect_selector("ansi"), None, None)
    for (idx, rule) in enumerate(_RULES):
        rule._eval(context)

I obviously don't know anything about RuleContext here: the idea is that you would do whatever you need to construct a valid instance, and limit symbolic state where possible.

Another important point; check out the page on "Hints for your Classes." These hints apply not only to the classes of arguments, but also to the classes that must be supplied to those classes, etc.

You might have considered testing at a higher level - perhaps something that takes a SQL string. CrossHair supports some reasoning about the builtin re module, but since SQLFluff uses a third party regex module written in C, it won't do anything useful with it. At this level, you're likely better off using something like Atheris.

Whew! Ok, so I'm hoping that you'll continue to let me know how this goes. Having more real-world use cases to guide CrossHair is near the top of my wishlist, so this has already been super helpful.

pschanely commented 2 years ago

The original NameError issue should be fixed in v.0.0.22.

Going to continue to leave this issue open, though, for additional SQLFluff discussion if we want it.

pschanely commented 2 years ago

Closing this old issue. Hope the fix worked for you, and don't hesitate to reach out again!