Skip to content

Commit f4f84cb

Browse files
dlreevesmeta-codesync[bot]
authored andcommitted
Reject Hack files placed directly under an implicit package family path
Summary: # Context An implicit package family declares a `path`, and each direct child directory `D` of that path denotes a synthesized member package `F.D`. A file sitting directly under the family path, with no intervening directory, therefore belongs to no member package at all. Nothing reported that. The file simply resolved to "no package", which silently exempts it from cross-package enforcement in both directions. It is an edge case the original design missed rather than a decision, so it should be an error. # Solution Add `Naming[2135]`, raised when a file lies directly under a family `path`. This check is done in the lowerer, which has access to the package information needed. The check is invoked from `p_script`, which runs exactly once per file before any definition is lowered, so a file that declares nothing is still checked. It is deliberately independent of `__PackageOverride`: an override cannot make a badly-placed file legal. The error is anchored at the very start of the file as a zero-width position. Nothing the file *contains* is at fault, so there is no text worth underlining -- and a fixed-width span would be wrong for a `.hack` file, which has no `<?hh` pragma and therefore begins with ordinary code. Reviewed By: madgen Differential Revision: D114385205 fbshipit-source-id: 6f15cab900cd2065f7471a95ec94420d2b0a3f15
1 parent c7ab4f5 commit f4f84cb

15 files changed

Lines changed: 226 additions & 16 deletions

hphp/hack/src/diagnostics/error_codes.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ module Naming = struct
173173
| TypedOpenShapeDisallowed [@value 2132]
174174
| NamedVariadicTypeDisallowed [@value 2133]
175175
| VariadicNamedParameterDisallowed [@value 2134]
176+
| ImplicitPackageFileDirectlyUnderPath [@value 2135]
176177
(* Add new Naming codes here! Comment out when deprecating. *)
177178
[@@deriving enum, show { with_path = false }]
178179

hphp/hack/src/oxidized/gen/error_codes.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This source code is licensed under the MIT license found in the
44
// LICENSE file in the "hack" directory of this source tree.
55
//
6-
// @generated SignedSource<<23a844de2b22a4423d0dc60ebaff4a51>>
6+
// @generated SignedSource<<aa25f2a5e885275c1761709806ae1743>>
77
//
88
// To regenerate this file, run:
99
// buck run @fbcode//mode/dev-nosan-lg fbcode//hphp/hack/src:oxidized_regen
@@ -147,6 +147,7 @@ pub enum Naming {
147147
TypedOpenShapeDisallowed = 2132,
148148
NamedVariadicTypeDisallowed = 2133,
149149
VariadicNamedParameterDisallowed = 2134,
150+
ImplicitPackageFileDirectlyUnderPath = 2135,
150151
}
151152
impl TrivialDrop for Naming {}
152153
arena_deserializer::impl_deserialize_in_arena!(Naming);

hphp/hack/src/oxidized/manual/diagnostics_impl.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,36 @@ impl Naming {
144144
)
145145
}
146146

147+
/// A Hack file placed directly under an implicit package family's `path`,
148+
/// with no member directory in between, belongs to no member package.
149+
///
150+
/// `family_pos` points at the family's declaration in PACKAGES.toml.
151+
pub fn implicit_package_file_directly_under_path(
152+
pos: Pos,
153+
family: &str,
154+
family_pos: Pos,
155+
) -> Diagnostic {
156+
UserDiagnostic::new(
157+
Severity::Err,
158+
Self::ImplicitPackageFileDirectlyUnderPath as isize,
159+
Message(
160+
pos,
161+
format!(
162+
"A Hack file cannot be placed directly under the implicit package family `{}`; move it into a subdirectory so it belongs to a member package `{}.<dir>`",
163+
family, family
164+
)
165+
.into(),
166+
),
167+
vec![Message(
168+
family_pos,
169+
format!("The implicit package family `{}` is declared here", family).into(),
170+
)],
171+
Explanation::Empty,
172+
vec![],
173+
vec![],
174+
)
175+
}
176+
147177
pub fn bad_builtin_type(p: Pos, name: &str, correct_name: &str) -> Diagnostic {
148178
UserDiagnostic::new(
149179
Severity::Err,

hphp/hack/src/oxidized/manual/package_info_impl.rs

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -168,36 +168,68 @@ static MULTIFILE_PREFIX: LazyLock<regex::Regex> = LazyLock::new(|| {
168168
regex::Regex::new(r"^.*--").expect("should compile: the pattern is a literal")
169169
});
170170

171+
/// The path rewriting every package lookup applies before prefix-matching. All
172+
/// lookups must go through this, so they prefix-match the same string.
173+
fn normalize(support_multifile_tests: bool, path: &str) -> Cow<'_, str> {
174+
if support_multifile_tests {
175+
MULTIFILE_PREFIX.replace(path, "")
176+
} else {
177+
Cow::Borrowed(path)
178+
}
179+
}
180+
171181
impl PackageInfo {
182+
/// The most precise package whose `include_path` prefixes `path`, together
183+
/// with the part of `path` lying below that `include_path`.
184+
fn match_include_path<'a, 'p>(&'a self, path: &'p str) -> Option<(&'a Package, &'p str)> {
185+
let (include_path, package) = self
186+
.include_path_to_package_map
187+
.iter()
188+
.find(|(include_path, _)| path.starts_with(include_path))?;
189+
Some((package, &path[include_path.len()..]))
190+
}
191+
172192
pub fn get_package_for_file(
173193
&self,
174194
support_multifile_tests: bool,
175195
path: &str,
176196
) -> Option<Cow<'_, Package>> {
177-
let path = if support_multifile_tests {
178-
MULTIFILE_PREFIX.replace(path, "")
179-
} else {
180-
Cow::Borrowed(path)
181-
};
182-
let (include_path, package) = self
183-
.include_path_to_package_map
184-
.iter()
185-
.find(|(include_path, _)| path.starts_with(include_path))?;
197+
let path = normalize(support_multifile_tests, path);
198+
let (package, remainder) = self.match_include_path(&path)?;
186199
if !package.is_implicit {
187200
// Common case: the stored package is returned by reference, no clone.
188201
return Some(Cow::Borrowed(package));
189202
}
190-
// Implicit family match: the member directory `D` is the first path
191-
// segment after the family `path`. Only direct child *directories*
192-
// denote members, so a file lying directly in the family path (no `/`
193-
// after the prefix) belongs to no package. The member is synthesized on
194-
// demand, hence owned.
195-
let remainder = &path[include_path.len()..];
203+
// The member directory `D` is the first segment after the family
204+
// `path`. Only child directories denote members, so a file lying
205+
// directly in the family path belongs to no package.
196206
match remainder.split_once('/') {
197207
Some((dir, _)) if !dir.is_empty() => Some(Cow::Owned(synthesize_member(package, dir))),
198208
_ => None,
199209
}
200210
}
211+
212+
/// The implicit family (its positioned name) if `path` lies directly under
213+
/// that family's `path`, with no member directory in between -- e.g.
214+
/// `//prototypes/loose.php` under a family declared at `//prototypes/`.
215+
///
216+
/// `None` means the file is legally placed: either it belongs to a member,
217+
/// or it is under no family at all.
218+
pub fn file_directly_under_implicit_family(
219+
&self,
220+
support_multifile_tests: bool,
221+
path: &str,
222+
) -> Option<&PosId> {
223+
let path = normalize(support_multifile_tests, path);
224+
let (package, remainder) = self.match_include_path(&path)?;
225+
if !package.is_implicit {
226+
return None;
227+
}
228+
match remainder.split_once('/') {
229+
Some((dir, _)) if !dir.is_empty() => None,
230+
_ => Some(&package.name),
231+
}
232+
}
201233
}
202234

203235
#[cfg(test)]

hphp/hack/src/parser/lowerer/lowerer.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6262,6 +6262,31 @@ fn get_current_package<'a>(env: &mut Env<'a>, node: S<'a>) -> Option<PackageMemb
62626262
package
62636263
}
62646264

6265+
/// A file under an implicit package family's `path` must sit inside a member
6266+
/// subdirectory; one placed directly under the family path belongs to no member
6267+
/// package, so it is illegal.
6268+
fn check_implicit_package_placement(env: &mut Env<'_>) {
6269+
let parser_options = env.parser_options;
6270+
let file_path = env.source_text().file_path_rc();
6271+
let Some(family) = parser_options
6272+
.package_info
6273+
.file_directly_under_implicit_family(
6274+
parser_options.package_support_multifile_tests,
6275+
file_path.path_str(),
6276+
)
6277+
else {
6278+
return;
6279+
};
6280+
let (family_pos, family_name) = (family.0.clone(), family.1.clone());
6281+
// Zero-width, at the start of the file: nothing the file contains is at
6282+
// fault, and a `.hack` file has no `<?hh` pragma to point at.
6283+
let file_top_pos = Pos::from_lnum_bol_offset(file_path, (1, 0, 0), (1, 0, 0));
6284+
raise_hh_error(
6285+
env,
6286+
Naming::implicit_package_file_directly_under_path(file_top_pos, &family_name, family_pos),
6287+
);
6288+
}
6289+
62656290
fn p_def<'a>(node: S<'a>, env: &mut Env<'a>) -> Result<Vec<ast::Def>> {
62666291
let doc_comment_opt = extract_docblock(node, env);
62676292
match &node.children {
@@ -7081,6 +7106,8 @@ fn p_program<'a>(node: S<'a>, env: &mut Env<'a>) -> ast::Program {
70817106
}
70827107

70837108
fn p_script<'a>(node: S<'a>, env: &mut Env<'a>) -> ast::Program {
7109+
// Once per file, so a file declaring nothing is still checked.
7110+
check_implicit_package_placement(env);
70847111
match &node.children {
70857112
Script(c) => p_program(&c.declarations, env),
70867113
_ => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//// prototypes/loose.hack
2+
// A `.hack` file has no `<?hh` pragma, so line 1 is ordinary code.
3+
const int LOOSE_HACK_C = 1;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
error: Naming[2135] A Hack file cannot be placed directly under the implicit package family `prototypes`; move it into a subdirectory so it belongs to a member package `prototypes.<dir>` [1]
2+
-> The implicit package family `prototypes` is declared here [2]
3+
4+
dothack_file_directly_under_path.php--prototypes/loose.hack:1:1
5+
1 | // A `.hack` file has no `<?hh` pragma, so line 1 is ordinary code.
6+
| ^ [1]
7+
2 | const int LOOSE_HACK_C = 1;
8+
9+
PACKAGES.toml:18:20
10+
16 | # (prototypes.<dir>). Members are isolated from each other but all hard-include
11+
17 | # intern (and transitively shared) and soft-include standalone.
12+
18 | [implicit_packages.prototypes]
13+
| ^^^^^^^^^^ [2]
14+
19 | path = "//prototypes/"
15+
20 | includes = ["intern", "shared"]
16+
17+
1 error found
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//// prototypes/loose.php
2+
<?hh
3+
// Directly under //prototypes/ with no member directory in between.
4+
const int LOOSE_C = 1;
5+
6+
//// prototypes/beta/b.php
7+
<?hh
8+
// In a member directory, so this one is fine.
9+
const int BETA_C = 1;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
error: Naming[2135] A Hack file cannot be placed directly under the implicit package family `prototypes`; move it into a subdirectory so it belongs to a member package `prototypes.<dir>` [1]
2+
-> The implicit package family `prototypes` is declared here [2]
3+
4+
file_directly_under_path.php--prototypes/loose.php:1:1
5+
1 | <?hh
6+
| ^ [1]
7+
2 | // Directly under //prototypes/ with no member directory in between.
8+
3 | const int LOOSE_C = 1;
9+
10+
PACKAGES.toml:18:20
11+
16 | # (prototypes.<dir>). Members are isolated from each other but all hard-include
12+
17 | # intern (and transitively shared) and soft-include standalone.
13+
18 | [implicit_packages.prototypes]
14+
| ^^^^^^^^^^ [2]
15+
19 | path = "//prototypes/"
16+
20 | includes = ["intern", "shared"]
17+
18+
1 error found
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//// prototypes/mod.php
2+
<?hh
3+
// The only top-level definition is a module declaration.
4+
new module foo {}

0 commit comments

Comments
 (0)