diff --git a/codegen/inferred_relationships.hack b/codegen/inferred_relationships.hack index 860e3bbaf..1be8b7c31 100644 --- a/codegen/inferred_relationships.hack +++ b/codegen/inferred_relationships.hack @@ -1,7 +1,7 @@ /** * This file is generated. Do not modify it manually! * - * @generated SignedSource<> + * @generated SignedSource<<0cb6442b34479c8ca9d81ac0cc1ec48a>> */ namespace Facebook\HHAST\__Private; @@ -923,6 +923,7 @@ const dict> INFERRED_RELATIONSHIPS = dict[ 'list|list_item>', 'list>', 'list>', + 'list|list_item>', 'list|list_item>', 'list|list_item|list_item>', 'list|list_item>', @@ -2392,9 +2393,11 @@ const dict> INFERRED_RELATIONSHIPS = dict[ 'parenthesized_expression', 'postfix_unary_expression', 'prefix_unary_expression', + 'qualified_name', 'scope_resolution_expression', 'shape_expression', 'subscript_expression', + 'token:name', 'variable', 'varray_intrinsic_expression', 'vector_intrinsic_expression', diff --git a/codegen/syntax/LambdaExpression.hack b/codegen/syntax/LambdaExpression.hack index d42846ae0..517d49143 100644 --- a/codegen/syntax/LambdaExpression.hack +++ b/codegen/syntax/LambdaExpression.hack @@ -1,7 +1,7 @@ /** * This file is generated. Do not modify it manually! * - * @generated SignedSource<<53d3c66bc52630f3f896cee468880038>> + * @generated SignedSource<> */ namespace Facebook\HHAST; use namespace Facebook\TypeAssert; @@ -319,9 +319,10 @@ final class LambdaExpression * FunctionCallExpression | IsExpression | KeysetIntrinsicExpression | * LambdaExpression | LiteralExpression | MemberSelectionExpression | * NullableAsExpression | ObjectCreationExpression | ParenthesizedExpression - * | PostfixUnaryExpression | PrefixUnaryExpression | + * | PostfixUnaryExpression | PrefixUnaryExpression | QualifiedName | * ScopeResolutionExpression | ShapeExpression | SubscriptExpression | - * VariableExpression | VarrayIntrinsicExpression | VectorIntrinsicExpression + * NameToken | VariableExpression | VarrayIntrinsicExpression | + * VectorIntrinsicExpression */ public function getBody(): ILambdaBody { return TypeAssert\instance_of(ILambdaBody::class, $this->_body); @@ -334,9 +335,10 @@ final class LambdaExpression * FunctionCallExpression | IsExpression | KeysetIntrinsicExpression | * LambdaExpression | LiteralExpression | MemberSelectionExpression | * NullableAsExpression | ObjectCreationExpression | ParenthesizedExpression - * | PostfixUnaryExpression | PrefixUnaryExpression | + * | PostfixUnaryExpression | PrefixUnaryExpression | QualifiedName | * ScopeResolutionExpression | ShapeExpression | SubscriptExpression | - * VariableExpression | VarrayIntrinsicExpression | VectorIntrinsicExpression + * NameToken | VariableExpression | VarrayIntrinsicExpression | + * VectorIntrinsicExpression */ public function getBodyx(): ILambdaBody { return $this->getBody(); diff --git a/codegen/syntax/QualifiedName.hack b/codegen/syntax/QualifiedName.hack index 5d7a431d9..22d31c6cb 100644 --- a/codegen/syntax/QualifiedName.hack +++ b/codegen/syntax/QualifiedName.hack @@ -1,7 +1,7 @@ /** * This file is generated. Do not modify it manually! * - * @generated SignedSource<<541eaff8e49bd5eaad1a9c5f7ce7f85d>> + * @generated SignedSource<> */ namespace Facebook\HHAST; use namespace Facebook\TypeAssert; @@ -12,7 +12,10 @@ use namespace HH\Lib\Dict; <<__ConsistentConstruct>> final class QualifiedName extends Node - implements INameishNode, __Private\IWrappableWithSimpleTypeSpecifier { + implements + ILambdaBody, + INameishNode, + __Private\IWrappableWithSimpleTypeSpecifier { const string SYNTAX_KIND = 'qualified_name'; diff --git a/codegen/tokens/NameToken.hack b/codegen/tokens/NameToken.hack index da2235ab4..5a4ac6e57 100644 --- a/codegen/tokens/NameToken.hack +++ b/codegen/tokens/NameToken.hack @@ -1,13 +1,16 @@ /** * This file is generated. Do not modify it manually! * - * @generated SignedSource<<5f91cc9b0eb7b319df6d8118854d35af>> + * @generated SignedSource<<69e54ae78c68fbded1ebb524ac6284c4>> */ namespace Facebook\HHAST; final class NameToken extends TokenWithVariableText - implements INameishNode, __Private\IWrappableWithSimpleTypeSpecifier { + implements + ILambdaBody, + INameishNode, + __Private\IWrappableWithSimpleTypeSpecifier { const string KIND = 'name'; diff --git a/src/Linters/NoEmptyStatementsLinter.hack b/src/Linters/NoEmptyStatementsLinter.hack index 3705be1a5..f23ef1ca4 100644 --- a/src/Linters/NoEmptyStatementsLinter.hack +++ b/src/Linters/NoEmptyStatementsLinter.hack @@ -131,7 +131,7 @@ final class NoEmptyStatementsLinter extends AutoFixingASTLinter { /** * Returns whether the given token is an assignment operator. * - * This list is all the types returned from ExpressionStatement::getOperator + * This list is all the types returned from BinaryExpression::getOperator * that include "Equal" and are not comparison operators (==, >=, etc.); */ private function isAssignmentOperator(Token $op): bool { @@ -140,9 +140,7 @@ final class NoEmptyStatementsLinter extends AutoFixingASTLinter { $op is CaratEqualToken || $op is DotEqualToken || $op is EqualToken || - $op is GreaterThanEqualToken || $op is GreaterThanGreaterThanEqualToken || - $op is LessThanEqualToken || $op is LessThanLessThanEqualToken || $op is MinusEqualToken || $op is PercentEqualToken || diff --git a/src/Linters/UnusedUseClauseLinter.hack b/src/Linters/UnusedUseClauseLinter.hack index 67be8bc1c..b63c3f186 100644 --- a/src/Linters/UnusedUseClauseLinter.hack +++ b/src/Linters/UnusedUseClauseLinter.hack @@ -55,8 +55,8 @@ final class UnusedUseClauseLinter extends AutoFixingASTLinter { $as = $name->getText(); } else { invariant($name is QualifiedName, 'Unhandled name type'); - $as = $name->getParts()->getChildrenOfItemsOfType(NameToken::class) - |> (C\lastx($$) as nonnull)->getText(); + $as = $name->getParts()->getChildrenOfItemsByType() + |> C\lastx($$)->getText(); } } if ($kind is NamespaceToken) { diff --git a/src/Linters/UseStatementWIthoutKindLinter.hack b/src/Linters/UseStatementWIthoutKindLinter.hack index f0425633f..6f6bff9cf 100644 --- a/src/Linters/UseStatementWIthoutKindLinter.hack +++ b/src/Linters/UseStatementWIthoutKindLinter.hack @@ -55,10 +55,8 @@ final class UseStatementWithoutKindLinter extends AutoFixingASTLinter { } $name = $clause->getName(); if ($name is QualifiedName) { - return ( - C\lastx( - $name->getParts()->getChildrenOfItemsOfType(NameToken::class), - ) as nonnull + return C\lastx( + $name->getParts()->getChildrenOfItemsByType(), )->getText(); } invariant( @@ -73,10 +71,8 @@ final class UseStatementWithoutKindLinter extends AutoFixingASTLinter { // We need to look at the full file to figure out if this should be a // `use type`, or `use namespace` $used = $this->getUnresolvedReferencedNames(); - $used_as_ns = C\any( - $names, - $name ==> C\contains($used['namespaces'], $name), - ); + $used_as_ns = + C\any($names, $name ==> C\contains($used['namespaces'], $name)); $used_as_type = C\any($names, $name ==> C\contains($used['types'], $name)); $leading = $node->getClauses()->getFirstTokenx()->getLeadingWhitespace(); @@ -94,8 +90,7 @@ final class UseStatementWithoutKindLinter extends AutoFixingASTLinter { } <<__Memoize>> - private function getUnresolvedReferencedNames( - ): shape( + private function getUnresolvedReferencedNames(): shape( 'namespaces' => keyset, 'types' => keyset, 'functions' => keyset, diff --git a/src/Migrations/HSLMigration.hack b/src/Migrations/HSLMigration.hack index 1f8163ab4..df0f10487 100644 --- a/src/Migrations/HSLMigration.hack +++ b/src/Migrations/HSLMigration.hack @@ -523,7 +523,7 @@ final class HSLMigration extends BaseMigration { } $found_prefix = true; foreach ($parts as $i => $token) { - if ($token?->getText() === $search[$i]) { + if ($token->getText() === $search[$i]) { continue; } $found_prefix = false; @@ -569,14 +569,14 @@ final class HSLMigration extends BaseMigration { } foreach ($parts as $i => $token) { - if ($i < 2 && $token?->getText() !== $search[$i]) { + if ($i < 2 && $token->getText() !== $search[$i]) { break; } if ($i === 2) { // we found an HH\Lib\* use statement, add the node and suffix $nodes[] = $decl; - $ns = HslNamespace::coerce($token?->getText()); + $ns = HslNamespace::coerce($token->getText()); if ($ns !== null) { $suffixes[] = $ns; } diff --git a/src/__Private/LintRunConfig.hack b/src/__Private/LintRunConfig.hack index 85a9a29ba..b185249f6 100644 --- a/src/__Private/LintRunConfig.hack +++ b/src/__Private/LintRunConfig.hack @@ -166,8 +166,8 @@ final class LintRunConfig { Vec\map($this->configFile['roots'], $dir ==> $this->projectRoot.'/'.$dir); } - private function findOverride(string $file_path): ?self::TOverride { - return C\find( + private function findOverrides(string $file_path): vec { + return Vec\filter( $this->configFile['overrides'] ?? vec[], $override ==> C\find( $override['patterns'], @@ -206,8 +206,7 @@ final class LintRunConfig { $blacklist = $this->configFile['disabledLinters'] ?? vec[]; $autofix_blacklist = $this->configFile['disabledAutoFixes'] ?? vec[]; $no_autofixes = $this->configFile['disableAllAutoFixes'] ?? false; - $override = $this->findOverride($file_path); - if ($override is nonnull) { + foreach ($this->findOverrides($file_path) as $override) { if ($override['disableAllLinters'] ?? false) { return shape( 'linters' => keyset[], @@ -264,7 +263,8 @@ final class LintRunConfig { $file_path is null ? null : $this->relativeFilePath($file_path) |> $$ is null ? null - : $this->findOverride($$)['linterConfigs'][$classname] ?? null; + // TODO: This doesn't support multiple overrides. + : C\first($this->findOverrides($$))['linterConfigs'][$classname] ?? null; if ($global_linter_config is null) { if ($file_linter_config is null) { return null; diff --git a/src/__Private/codegen/CodegenBase.hack b/src/__Private/codegen/CodegenBase.hack index 3af245e0a..6f5687978 100644 --- a/src/__Private/codegen/CodegenBase.hack +++ b/src/__Private/codegen/CodegenBase.hack @@ -217,6 +217,9 @@ abstract class CodegenBase { HHAST\ILambdaBody::class => keyset[ HHAST\IExpression::class, HHAST\CompoundStatement::class, + // Constants are not wrapped in a name expression on the RHS of `==>`. + HHAST\NameToken::class, + HHAST\QualifiedName::class, ], HHAST\ILambdaSignature::class => keyset[ HHAST\VariableExpression::class, @@ -239,8 +242,6 @@ abstract class CodegenBase { HHAST\ParameterDeclaration::class, HHAST\PropertyDeclaration::class, HHAST\LambdaExpression::class, - // HHAST\Php7AnonymousFunction::class : not valid in hack. No attributes - // if not hack ], HHAST\INameishNode::class => keyset[ HHAST\NameToken::class, diff --git a/src/__Private/codegen/CodegenRelations.hack b/src/__Private/codegen/CodegenRelations.hack index 85ef25125..914f41ac5 100644 --- a/src/__Private/codegen/CodegenRelations.hack +++ b/src/__Private/codegen/CodegenRelations.hack @@ -60,7 +60,7 @@ final class CodegenRelations extends CodegenBase { $relationships = new Ref(dict[]); $queue = new Async\Semaphore( - /* limit = */ 32, + \cpu_get_count(), async $file ==> { try { $links = await $this->getRelationsInFileAsync($file); diff --git a/src/__Private/codegen/data/LambdaBody.SyntaxExample.hack b/src/__Private/codegen/data/LambdaBody.SyntaxExample.hack new file mode 100644 index 000000000..9b79c5280 --- /dev/null +++ b/src/__Private/codegen/data/LambdaBody.SyntaxExample.hack @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ +namespace Facebook\HHAST\__Private\SyntaxExamples; + +function lambda_body(): void { + $_ = () ==> Qualified\CONSTANT; + $_ = () ==> CONSTANT; + $_ = () ==> 1 + CONSTANT; +} diff --git a/src/nodes/NodeList.hack b/src/nodes/NodeList.hack index bac6b8b7b..13f05f6b5 100644 --- a/src/nodes/NodeList.hack +++ b/src/nodes/NodeList.hack @@ -10,14 +10,23 @@ namespace Facebook\HHAST; use namespace HH\Lib\{C, Str, Vec}; +use type Facebook\HHAST\_Private\SoftDeprecated; /* HHAST_IGNORE_ALL[5624] */ final class NodeList<+Titem as Node> extends Node { const string SYNTAX_KIND = 'list'; /** - * Use `NodeList::createMaybeEmptyList()` or - * `NodeList::createNonEmptyListNull()` instead to be explicit - * about desired behavior. + * Use `NodeList::createMaybeEmptyList(vec[])` or `null` instead of + * `new NodeList(vec[])` if you know the vec is always empty. + * A parsed Hack AST doesn't contain empty NodeLists. + * + * Side note: Places where you'd expect to find an empty NodeList: + * ``` + * function no_params( ): void {} + * // ^ + * ``` + * The Hack parser places a "missing" node at the carat. + * HHAST uses `null` to represent them. */ <<__Override>> public function __construct( @@ -42,17 +51,33 @@ final class NodeList<+Titem as Node> extends Node { return $this->_children; } - public function getChildrenOfItems( - ): vec where Titem as ListItem { + public function getChildrenOfItems(): vec + where + Titem as ListItem { return Vec\map($this->getChildren(), $child ==> $child->getItem()); } + <getChildrenOfItemsByType()')>> public function getChildrenOfItemsOfType( classname $what, - ): vec where Titem as ListItem { + ): vec where Titem as ListItem { $out = vec[]; foreach ($this->getChildrenOfItems() as $item) { if (\is_a($item, $what)) { + $out[] = \HH\FIXME\UNSAFE_CAST( + $item, + 'is_a($item, $what) ~= $item is T', + ); + } + } + return $out; + } + + public function getChildrenOfItemsByType<<<__Enforceable>> reify T as Node>( + ): vec where Titem as ListItem { + $out = vec[]; + foreach ($this->getChildrenOfItems() as $item) { + if ($item is T) { $out[] = $item; } } diff --git a/test-data/hhast-lint.json b/test-data/hhast-lint.json index 62aea03fd..34a5b6e30 100644 --- a/test-data/hhast-lint.json +++ b/test-data/hhast-lint.json @@ -1,5 +1,6 @@ { "roots": [ "." ], + "builtinLinters": "none", "extraLinters": [ "Facebook\\HHAST\\Tests\\ValidConfigForLinter", "Facebook\\HHAST\\Tests\\InvalidConfigForLinter", @@ -27,5 +28,33 @@ "Facebook\\HHAST\\Tests\\ConfigTypeIsNotSupportedByTypeAssertLinter": { "impossible": [] } - } -} \ No newline at end of file + }, + "overrides": [ + { + "patterns": [ + "single_override/*", + "multiple_overrides/*" + ], + "extraLinters": [ + "Facebook\\HHAST\\NoEmptyStatementsLinter", + "Facebook\\HHAST\\UseStatementWIthoutKindLinter" + ], + "disabledLinters": [ + "Facebook\\HHAST\\Tests\\ValidConfigForLinter", + "Facebook\\HHAST\\Tests\\InvalidConfigForLinter" + ] + }, + { + "patterns": [ + "multiple_overrides/*" + ], + "extraLinters": [ + "Facebook\\HHAST\\NoFinalMethodInFinalClassLinter" + ], + "disabledLinters": [ + "Facebook\\HHAST\\FinalOrAbstractClassLinter", + "Facebook\\HHAST\\Tests\\ConfigTypeIsNotSupportedByTypeAssertLinter" + ] + } + ] +} diff --git a/tests/LinterConfigTest.hack b/tests/LinterConfigTest.hack index 2916c57c8..19d24fb0f 100644 --- a/tests/LinterConfigTest.hack +++ b/tests/LinterConfigTest.hack @@ -146,4 +146,35 @@ final class LinterConfigTest extends TestCase { 'specified an unsupported config type', ); } + + public function testSingleOverride(): void { + $lrc = static::getLintRunConfig(); + $config = $lrc->getConfigForFile('single_override/test.hack'); + + expect($config)->toNotBeNull('Config could not be fetched'); + expect($config)->toEqual(shape( + 'linters' => keyset [ + 'Facebook\\HHAST\\FinalOrAbstractClassLinter', + 'Facebook\\HHAST\\Tests\\ConfigTypeIsNotSupportedByTypeAssertLinter', + 'Facebook\\HHAST\\NoEmptyStatementsLinter', + 'Facebook\\HHAST\\UseStatementWIthoutKindLinter', + ], + 'autoFixBlacklist' => keyset [], + )); + } + + public function testMultipleOverrides(): void { + $lrc = static::getLintRunConfig(); + $config = $lrc->getConfigForFile('multiple_overrides/test.hack'); + + expect($config)->toNotBeNull('Config could not be fetched'); + expect($config)->toEqual(shape( + 'linters' => keyset [ + 'Facebook\\HHAST\\NoEmptyStatementsLinter', + 'Facebook\\HHAST\\UseStatementWIthoutKindLinter', + 'Facebook\\HHAST\\NoFinalMethodInFinalClassLinter', + ], + 'autoFixBlacklist' => keyset [], + )); + } } diff --git a/tests/NoParamsIsAMissingNodeTest.hack b/tests/NoParamsIsAMissingNodeTest.hack new file mode 100644 index 000000000..e98877797 --- /dev/null +++ b/tests/NoParamsIsAMissingNodeTest.hack @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\HHAST; + +use type Facebook\HackTest\HackTest; +use function Facebook\FBExpect\expect; + +final class NoParamsIsAMissingNodeTest extends HackTest { + /** + * @see `NodeList::__construct()` + * If this test fails, don't try and fix it. + * Just remove the comment (and this test) if this ever starts failing. + */ + public function testTheFollowingCommentStaysCorrect(): void { + $_error = null; + $json = \HH\ffp_parse_string('function no_params( ): void {}') + |> \json_encode_pure($$, inout $_error); + expect($json)->toContainSubstring( + '"function_parameter_list":{"kind":"missing"}', + ); + } +} diff --git a/tests/NodeListGetChildrenOfItemsOfTypeTest.hack b/tests/NodeListGetChildrenOfItemsOfTypeTest.hack new file mode 100644 index 000000000..2c6ec9bb7 --- /dev/null +++ b/tests/NodeListGetChildrenOfItemsOfTypeTest.hack @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\HHAST; + +use namespace HH\Lib\C; +use type Facebook\HackTest\HackTest; + +final class NodeListGetChildrenOfItemsOfTypeTest extends HackTest { + public function testRefinesType(): void { + $node_list = static::getNodeListOfItemsOfIExpression(); + $literal_expression = + $node_list->getChildrenOfItemsOfType(LiteralExpression::class) + |> C\firstx($$); + static::takesT($literal_expression); + } + + public function testReplacementRefinesTypeToo(): void { + $node_list = static::getNodeListOfItemsOfIExpression(); + $literal_expression = + $node_list->getChildrenOfItemsByType() |> C\firstx($$); + static::takesT($literal_expression); + } + + private static function getNodeListOfItemsOfIExpression( + ): NodeList> { + // NodeList(123,) + return new NodeList( + vec[new ListItem( + new LiteralExpression(new DecimalLiteralToken(null, null, '123')), + new CommaToken(null, null), + )], + ); + } + + private static function takesT<<<__Explicit>> T>(T $_): void {} +} diff --git a/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.autofix.expect b/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.autofix.expect new file mode 100644 index 000000000..d11cb5e31 --- /dev/null +++ b/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.autofix.expect @@ -0,0 +1,8 @@ + 4; + 3 >= 4; +} diff --git a/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.expect b/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.expect new file mode 100644 index 000000000..1fbbe0cea --- /dev/null +++ b/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.expect @@ -0,0 +1,22 @@ +[ + { + "blame": " 3 < 4;\n", + "blame_pretty": " 3 < 4;\n", + "description": "This statement includes an expression that has no effect" + }, + { + "blame": " 3 <= 4;\n", + "blame_pretty": " 3 <= 4;\n", + "description": "This statement includes an expression that has no effect" + }, + { + "blame": " 3 > 4;\n", + "blame_pretty": " 3 > 4;\n", + "description": "This statement includes an expression that has no effect" + }, + { + "blame": " 3 >= 4;\n", + "blame_pretty": " 3 >= 4;\n", + "description": "This statement includes an expression that has no effect" + } +] diff --git a/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.in b/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.in new file mode 100644 index 000000000..d11cb5e31 --- /dev/null +++ b/tests/examples/NoEmptyStatementsLinter/comparison_empty_statements.php.in @@ -0,0 +1,8 @@ + 4; + 3 >= 4; +}