Repository: pantsbuild/pants

Issue:
Lint Goal Failing on  pants.engine.internals.build_files.resolve_address Because of InvalidTargetName
We have been using `pants` for a while. Some of our oldest code that was ported to `pants` and runs fine under v1.25 is having trouble under v2.0.0.

Is `house/cellar` an invalid name for a `python_library` target?
```
python_library(
    name="house/cellar",
    sources=[
...
```

When I run `./pants --changed-since='origin/master' lint` with changes in projects that contains targets with `/` in their name, I trigger the following code: https://github.com/pantsbuild/pants/blob/79e2b09b8d077ff1f1d42ef97dcf0a8ee25fd455/src/python/pants/build_graph/address.py#L172-L178

Part of the error output looks like:
```
Engine traceback:
  in select
  in pants.core.goals.lint.lint
  in pants.engine.internals.graph.resolve_targets
  in pants.engine.internals.graph.resolve_unexpanded_targets
  in pants.engine.internals.build_files.strip_address_origins
  in pants.engine.internals.graph.resolve_addresses_with_origins
  in pants.engine.internals.build_files.addresses_with_origins_from_address_specs
  in pants.engine.internals.build_files.resolve_address
Traceback (most recent call last):
  File "/Users/myaccount/.cache/pants/setup/bootstrap-Darwin-x86_64/2.0.0_py36/lib/python3.6/site-packages/pants/engine/internals/native.py", line 69, in generator_send
    res = func.send(arg)
  File "/Users/myaccount/.cache/pants/setup/bootstrap-Darwin-x86_64/2.0.0_py36/lib/python3.6/site-packages/pants/engine/internals/build_files.py", line 66, in resolve_address
    return address_input.file_to_address()
  File "/Users/myaccount/.cache/pants/setup/bootstrap-Darwin-x86_64/2.0.0_py36/lib/python3.6/site-packages/pants/build_graph/address.py", line 176, in file_to_address
    "A target may only be defined in a directory containing a file that it owns in "
pants.build_graph.address.InvalidTargetName: A target may only be defined in a directory containing a file that it owns in the filesystem: `../../house/cellar` is not at-or-above the file `house/cellar/__init__.py`.
```

When I run `./pants lint //:house/cellar` it works as expected. 

## Added Complication That Might Be Very Relevant
This target is defined in a `BUILD` file at the top of the repo. When I ran debug, I get back changed addresses that look like `Address(//action/cellar/mouse.py:../../action/cellar_tests)`. Shouldn't it look like `Address(//action/cellar/mouse.py://action/cellar_tests)`? I don't think whatever is generating the `Address()` objects is handling targets defined in top level `BUILD` files correctly.


Discussion:
In trying to find a way to tell which commit causes it to fail, I ran `./pants --changed-since='master' list`. It fails on `list`, too.
I think I've confirmed this is the problem:
```
In [5]: parent_count = 2
    ...: expected_prefix = f"..{os.path.sep}" * parent_count
    ...: target_component = "../../house_cellar"
    ...: if target_component[: target_component.rfind(os.path.sep) + 1] != expected_prefix:
    ...:    print("error")
    ...: else:
    ...:     print("don't panic")
    ...:
don't panic

In [6]: parent_count = 2
    ...: expected_prefix = f"..{os.path.sep}" * parent_count
    ...: target_component = "../../house/cellar"
    ...: if target_component[: target_component.rfind(os.path.sep) + 1] != expected_prefix:
    ...:    print("error")
    ...: else:
    ...:     print("don't panic")
    ...:
error
```

If you have the target name (`house/cellar`) without the `../../` prefix, then this check can take into account target names with `/`. Otherwise, we need to make names with `/` in them invalid.

Why is this check done? What parts of the codebase won't work if we violate this rule: "A target may only be defined in a directory containing a file that it owns in the filesystem"?
I've verified that this did not produce an error in v1.25.

Resolving patch:
diff --git a/src/python/pants/build_graph/address.py b/src/python/pants/build_graph/address.py
--- a/src/python/pants/build_graph/address.py
+++ b/src/python/pants/build_graph/address.py
@@ -6,12 +6,13 @@
 from pathlib import PurePath
 from typing import Optional, Sequence
 
+from pants.base.deprecated import warn_or_error
 from pants.engine.engine_aware import EngineAwareParameter
 from pants.util.dirutil import fast_relpath, longest_dir_prefix
 from pants.util.strutil import strip_prefix
 
 # Currently unused, but reserved for possible future needs.
-BANNED_CHARS_IN_TARGET_NAME = frozenset("@!?=")
+BANNED_CHARS_IN_TARGET_NAME = frozenset(r"@!?/\:=")
 
 
 class InvalidSpecPath(ValueError):
@@ -40,13 +41,6 @@ def __post_init__(self):
                     f"Address spec {self.path_component}:{self.target_component} has no name part."
                 )
 
-            banned_chars = BANNED_CHARS_IN_TARGET_NAME & set(self.target_component)
-            if banned_chars:
-                raise InvalidTargetName(
-                    f"Banned chars found in target name. {banned_chars} not allowed in target "
-                    f"name: {self.target_component}"
-                )
-
         # A root is okay.
         if self.path_component == "":
             return
@@ -233,10 +227,35 @@ def __init__(
         """
         self.spec_path = spec_path
         self._relative_file_path = relative_file_path
+
         # If the target_name is the same as the default name would be, we normalize to None.
-        self._target_name = (
-            target_name if target_name and target_name != os.path.basename(self.spec_path) else None
-        )
+        self._target_name: Optional[str]
+        if target_name and target_name != os.path.basename(self.spec_path):
+            banned_chars = BANNED_CHARS_IN_TARGET_NAME & set(target_name)
+            deprecated_banned_chars = banned_chars & set(r"/\:")
+            if deprecated_banned_chars:
+                warn_or_error(
+                    removal_version="2.2.0.dev1",
+                    deprecated_entity_description=(
+                        r"Using any of the `\`, `/`, or `:` characters in a target name."
+                    ),
+                    hint=(
+                        f"The target name {target_name} (defined in directory {self.spec_path}) "
+                        f"contains deprecated characters (`{deprecated_banned_chars}`), which will "
+                        "cause some usecases to fail. Please replace these characters with another "
+                        "separator character like `_` or `-`."
+                    ),
+                )
+            elif banned_chars:
+                raise InvalidTargetName(
+                    f"The target name {target_name} (defined in directory {self.spec_path}) "
+                    f"contains banned characters (`{banned_chars}`). Please replace these "
+                    "characters with another separator character like `_` or `-`."
+                )
+            self._target_name = target_name
+        else:
+            self._target_name = None
+
         self._hash = hash((self.spec_path, self._relative_file_path, self._target_name))
         if PurePath(spec_path).name.startswith("BUILD"):
             raise InvalidSpecPath(


Test patch:
diff --git a/src/python/pants/build_graph/address_test.py b/src/python/pants/build_graph/address_test.py
--- a/src/python/pants/build_graph/address_test.py
+++ b/src/python/pants/build_graph/address_test.py
@@ -1,6 +1,7 @@
 # Copyright 2014 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
+import warnings
 from typing import Optional
 
 import pytest
@@ -93,10 +94,10 @@ def assert_bad_path_component(spec: str) -> None:
     assert_bad_path_component("///a")
 
 
-def test_address_input_parse_bad_target_component() -> None:
+def test_address_bad_target_component() -> None:
     def assert_bad_target_component(spec: str) -> None:
         with pytest.raises(InvalidTargetName):
-            repr(AddressInput.parse(spec))
+            repr(AddressInput.parse(spec).dir_to_address())
 
     # Missing target_component
     assert_bad_target_component("")
@@ -111,6 +112,12 @@ def assert_bad_target_component(spec: str) -> None:
     assert_bad_target_component("//:?t")
     assert_bad_target_component("//:=t")
 
+    # Deprecated banned chars. This should convert into an error in `2.2.0.dev1`.
+    with warnings.catch_warnings(record=True) as w:
+        AddressInput.parse(r"a:b\c").dir_to_address()
+        assert len(w) == 1
+        assert "deprecated" in str(w[0].message)
+
 
 def test_subproject_spec() -> None:
     # Ensure that a spec referring to a subproject gets assigned to that subproject properly.