diff --git a/docs/source/_static/img/python_scopes.png b/docs/source/_static/img/python_scopes.png index d5ad158f1..0c1b0266e 100644 Binary files a/docs/source/_static/img/python_scopes.png and b/docs/source/_static/img/python_scopes.png differ diff --git a/docs/source/_static/img/python_scopes.svg b/docs/source/_static/img/python_scopes.svg index 5865e27de..a1c04911e 100644 --- a/docs/source/_static/img/python_scopes.svg +++ b/docs/source/_static/img/python_scopes.svg @@ -1,6 +1,4 @@ - - + inkscape:version="1.0.2 (e86c8708, 2021-01-15)" + sodipodi:docname="drawing.svg" + inkscape:export-filename="/Users/lpetre/Desktop/rect846-0.png" + inkscape:export-xdpi="191.53999" + inkscape:export-ydpi="191.53999"> - - - + @@ -64,222 +59,439 @@ - - - + id="layer1"> + + + + + builtin scope + + + + class range(stop) ... + + + inkscape:groupmode="layer" + id="layer2" + inkscape:label="global" + style="display:inline"> - + + + global scope + style="font-style:normal;font-variant:normal;font-weight:300;font-stretch:normal;font-size:5.64444000000000035px;line-height:125%;font-family:'Source Sans Pro';-inkscape-font-specification:'Source Sans Pro Light';text-align:end;letter-spacing:0px;word-spacing:0px;text-anchor:end;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458300000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;" + xml:space="preserve">global scope + + + + ITERATIONS = 10Cls().fn() + + inkscape:groupmode="layer" + id="layer4" + inkscape:label="class" + style="display:inline"> - + + + class scope + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.64444000000000035px;line-height:125%;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Medium';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:end;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:end;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458300000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;" + xml:space="preserve">class scope + + + + class Cls: class_attribute = 20 + + inkscape:groupmode="layer" + id="layer5" + inkscape:label="function" + style="display:inline"> - + + + function scope + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.64444000000000035px;line-height:125%;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Medium';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:end;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:end;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458300000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;" + xml:space="preserve">function scope + + + + def fn(): for i in range(ITERATIONS): ... + - + inkscape:groupmode="layer" + id="layer6" + inkscape:label="comprehension" + style="display:inline"> - + + + comprehension scope + style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.64444000000000035px;line-height:125%;font-family:'Helvetica Neue';-inkscape-font-specification:'Helvetica Neue Medium';font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:end;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:0px;word-spacing:0px;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:end;white-space:normal;shape-padding:0;opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.26458300000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;" + xml:space="preserve">comprehension scope + + + + return [ i for i in range(10) ] + - - - ITERATIONS = 10class Cls: class_attribute = 20 def fn(): for i in range(ITERATIONS): ... return [ i for i in range(10) ]Cls().fn() - diff --git a/docs/source/metadata.rst b/docs/source/metadata.rst index f6c9c0784..0db0e7f31 100644 --- a/docs/source/metadata.rst +++ b/docs/source/metadata.rst @@ -40,8 +40,8 @@ The wrapper provides a :func:`~libcst.metadata.MetadataWrapper.resolve` function .. autoclass:: libcst.metadata.MetadataWrapper :special-members: __init__ -If you're working with visitors, which extend :class:`~libcst.MetadataDependent`, -metadata dependencies will be automatically computed when visited by a +If you're working with visitors, which extend :class:`~libcst.MetadataDependent`, +metadata dependencies will be automatically computed when visited by a :class:`~libcst.metadata.MetadataWrapper` and are accessible through :func:`~libcst.MetadataDependent.get_metadata` @@ -134,14 +134,15 @@ New scopes are created for classes, functions, and comprehensions. Other block constructs like conditional statements, loops, and try…except don't create their own scope. -There are four different type of scope in Python: +There are five different type of scope in Python: +:class:`~libcst.metadata.BuiltinScope`, :class:`~libcst.metadata.GlobalScope`, :class:`~libcst.metadata.ClassScope`, :class:`~libcst.metadata.FunctionScope`, and :class:`~libcst.metadata.ComprehensionScope`. .. image:: _static/img/python_scopes.png - :alt: Diagram showing how the above 4 scopes are nested in each other + :alt: Diagram showing how the above 5 scopes are nested in each other :width: 400 :align: center @@ -175,6 +176,9 @@ assigned or accessed within. :no-undoc-members: :special-members: __contains__, __getitem__, __iter__ +.. autoclass:: libcst.metadata.BuiltinScope + :no-undoc-members: + .. autoclass:: libcst.metadata.GlobalScope :no-undoc-members: diff --git a/libcst/metadata/__init__.py b/libcst/metadata/__init__.py index 477a631fb..01e2514b6 100644 --- a/libcst/metadata/__init__.py +++ b/libcst/metadata/__init__.py @@ -33,6 +33,7 @@ Assignments, BaseAssignment, BuiltinAssignment, + BuiltinScope, ClassScope, ComprehensionScope, FunctionScope, @@ -60,6 +61,7 @@ "BaseAssignment", "Assignment", "BuiltinAssignment", + "BuiltinScope", "Access", "Scope", "GlobalScope", diff --git a/libcst/metadata/scope_provider.py b/libcst/metadata/scope_provider.py index 77ceafd17..919810ea5 100644 --- a/libcst/metadata/scope_provider.py +++ b/libcst/metadata/scope_provider.py @@ -338,13 +338,12 @@ def find_qualified_name_for_non_import( name_prefixes.append(scope.name) elif isinstance(scope, FunctionScope): name_prefixes.append(f"{scope.name}.") - elif isinstance(scope, GlobalScope): - break elif isinstance(scope, ComprehensionScope): name_prefixes.append("") - else: + elif not isinstance(scope, (GlobalScope, BuiltinScope)): raise Exception(f"Unexpected Scope: {scope}") - scope = scope.parent + + scope = scope.parent if scope.parent != scope else None parts = [*reversed(name_prefixes)] if remaining_name: @@ -536,27 +535,57 @@ def accesses(self) -> Accesses: return Accesses(self._accesses) +class BuiltinScope(Scope): + """ + A BuiltinScope represents python builtin declarations. See https://docs.python.org/3/library/builtins.html + """ + + def __init__(self, globals: Scope) -> None: + self.globals: Scope = globals # must be defined before Scope.__init__ is called + super().__init__(parent=self) + + def __contains__(self, name: str) -> bool: + return hasattr(builtins, name) + + def __getitem__(self, name: str) -> Set[BaseAssignment]: + if name in self._assignments: + return self._assignments[name] + if hasattr(builtins, name): + # note - we only see the builtin assignments during the deferred + # access resolution. unfortunately that means we have to create the + # assignment here, which can cause the set to mutate during iteration + self._assignments[name].add(BuiltinAssignment(name, self)) + return self._assignments[name] + return set() + + def record_assignment(self, name: str, node: cst.CSTNode) -> None: + raise NotImplementedError("assignments in builtin scope are not allowed") + + def record_global_overwrite(self, name: str) -> None: + raise NotImplementedError("global overwrite in builtin scope are not allowed") + + def record_nonlocal_overwrite(self, name: str) -> None: + raise NotImplementedError("declarations in builtin scope are not allowed") + + class GlobalScope(Scope): """ A GlobalScope is the scope of module. All module level assignments are recorded in GlobalScope. """ def __init__(self) -> None: - self.globals: Scope = self # must be defined before Scope.__init__ is called - super().__init__(parent=self) + super().__init__(parent=BuiltinScope(self)) def __contains__(self, name: str) -> bool: - return hasattr(builtins, name) or ( - name in self._assignments and len(self._assignments[name]) > 0 - ) + if name in self._assignments: + return len(self._assignments[name]) > 0 + return self.parent._contains_in_self_or_parent(name) def __getitem__(self, name: str) -> Set[BaseAssignment]: - if hasattr(builtins, name): - if not any( - isinstance(i, BuiltinAssignment) for i in self._assignments[name] - ): - self._assignments[name].add(BuiltinAssignment(name, self)) - return self._assignments[name] + if name in self._assignments: + return self._assignments[name] + else: + return self.parent._getitem_from_self_or_parent(name) def record_global_overwrite(self, name: str) -> None: pass diff --git a/libcst/metadata/tests/test_scope_provider.py b/libcst/metadata/tests/test_scope_provider.py index 27a8f495c..59a20aec7 100644 --- a/libcst/metadata/tests/test_scope_provider.py +++ b/libcst/metadata/tests/test_scope_provider.py @@ -13,6 +13,8 @@ from libcst.metadata import MetadataWrapper from libcst.metadata.scope_provider import ( Assignment, + BuiltinAssignment, + BuiltinScope, ClassScope, ComprehensionScope, FunctionScope, @@ -144,6 +146,11 @@ def fn(): self.assertEqual(len(scope_of_module[builtin]), 1) self.assertEqual(len(scope_of_module["something_not_a_builtin"]), 0) + scope_of_builtin = scope_of_module.parent + self.assertIsInstance(scope_of_builtin, BuiltinScope) + self.assertEqual(len(scope_of_builtin[builtin]), 1) + self.assertEqual(len(scope_of_builtin["something_not_a_builtin"]), 0) + func_body = ensure_type(m.body[0], cst.FunctionDef).body func_pass_statement = func_body.body[0] scope_of_func_statement = scopes[func_pass_statement] @@ -1687,3 +1694,70 @@ def test_cast(self) -> None: cast("3rr0r", "") """ ) + + def test_builtin_scope(self) -> None: + m, scopes = get_scope_metadata_provider( + """ + a = pow(1, 2) + def foo(): + b = pow(2, 3) + """ + ) + scope_of_module = scopes[m] + self.assertIsInstance(scope_of_module, GlobalScope) + self.assertEqual(len(scope_of_module["pow"]), 1) + builtin_pow_assignment = list(scope_of_module["pow"])[0] + self.assertIsInstance(builtin_pow_assignment, BuiltinAssignment) + self.assertIsInstance(builtin_pow_assignment.scope, BuiltinScope) + + global_a_assignments = scope_of_module["a"] + self.assertEqual(len(global_a_assignments), 1) + a_assignment = list(global_a_assignments)[0] + self.assertIsInstance(a_assignment, Assignment) + + func_body = ensure_type(m.body[1], cst.FunctionDef).body + func_statement = func_body.body[0] + scope_of_func_statement = scopes[func_statement] + self.assertIsInstance(scope_of_func_statement, FunctionScope) + func_b_assignments = scope_of_func_statement["b"] + self.assertEqual(len(func_b_assignments), 1) + b_assignment = list(func_b_assignments)[0] + self.assertIsInstance(b_assignment, Assignment) + + builtin_pow_accesses = list(builtin_pow_assignment.references) + self.assertEqual(len(builtin_pow_accesses), 2) + + def test_override_builtin_scope(self) -> None: + m, scopes = get_scope_metadata_provider( + """ + def pow(x, y): + return x ** y + + a = pow(1, 2) + def foo(): + b = pow(2, 3) + """ + ) + scope_of_module = scopes[m] + self.assertIsInstance(scope_of_module, GlobalScope) + self.assertEqual(len(scope_of_module["pow"]), 1) + global_pow_assignment = list(scope_of_module["pow"])[0] + self.assertIsInstance(global_pow_assignment, Assignment) + self.assertIsInstance(global_pow_assignment.scope, GlobalScope) + + global_a_assignments = scope_of_module["a"] + self.assertEqual(len(global_a_assignments), 1) + a_assignment = list(global_a_assignments)[0] + self.assertIsInstance(a_assignment, Assignment) + + func_body = ensure_type(m.body[2], cst.FunctionDef).body + func_statement = func_body.body[0] + scope_of_func_statement = scopes[func_statement] + self.assertIsInstance(scope_of_func_statement, FunctionScope) + func_b_assignments = scope_of_func_statement["b"] + self.assertEqual(len(func_b_assignments), 1) + b_assignment = list(func_b_assignments)[0] + self.assertIsInstance(b_assignment, Assignment) + + global_pow_accesses = list(global_pow_assignment.references) + self.assertEqual(len(global_pow_accesses), 2)