src/Linters/UnusedUseClauseLinter.hack (167 lines of code) (raw):
/*
* 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, Keyset, Str, Vec};
final class UnusedUseClauseLinter extends AutoFixingASTLinter {
const type TConfig = shape();
const type TNode = INamespaceUseDeclaration;
const type TContext = Script;
<<__Override>>
public function getLintErrorForNode(
Script $_context,
INamespaceUseDeclaration $node,
): ?ASTLintError {
$clauses = $node->getClauses()->getChildrenOfItems();
$unused = $this->getUnusedClauses($node->getKind(), $clauses);
if (C\is_empty($unused)) {
return null;
}
return new ASTLintError(
$this,
$unused
|> Vec\map($$, $p ==> '`'.$p[0].'`')
|> Str\join($$, ', ')
|> $$.((C\count($unused) === 1) ? ' is' : ' are').' not used',
$node,
() ==> $this->getFixedNode($node),
);
}
private function getUnusedClauses(
?Token $kind,
vec<NamespaceUseClause> $clauses,
): vec<(string, NamespaceUseClause)> {
$used = $this->getUnresolvedReferencedNames();
$unused = vec[];
foreach ($clauses as $clause) {
$as = $clause->getAlias();
if ($as !== null) {
$as = $as->getText();
} else {
$name = $clause->getName();
if ($name is NameToken) {
$as = $name->getText();
} else {
invariant($name is QualifiedName, 'Unhandled name type');
$as = $name->getParts()->getChildrenOfItemsOfType(NameToken::class)
|> (C\lastx($$) as nonnull)->getText();
}
}
if ($kind is NamespaceToken) {
if (!C\contains($used['namespaces'], $as)) {
$unused[] = tuple($as, $clause);
}
continue;
}
if ($kind is TypeToken) {
if (!C\contains($used['types'], $as)) {
$unused[] = tuple($as, $clause);
}
continue;
}
if ($kind is FunctionToken) {
if (!C\contains($used['functions'], $as)) {
$unused[] = tuple($as, $clause);
}
continue;
}
if ($kind is ConstToken) {
// unsupported
continue;
}
invariant($kind === null, 'Unhandled kind: %s', \get_class($kind));
if (C\contains($used['namespaces'], $as)) {
continue;
}
if (C\contains($used['types'], $as)) {
continue;
}
$unused[] = tuple($as, $clause);
}
return $unused;
}
<<__Override>>
protected function getTitleForFix(ASTLintError $error): string {
$node = $error->getBlameNode() as this::TNode;
$clauses = $node->getClauses()->getChildrenOfItems();
$unused = $this->getUnusedClauses($node->getKind(), $clauses);
if (C\count($clauses) === C\count($unused)) {
return 'Remove `use` statement';
}
return $this->getUnusedClauses($node->getKind(), $clauses)
|> Vec\map($$, $p ==> '`'.$p[1]->getCode().'`')
|> Str\join($$, ', ')
|> 'Remove '.$$;
}
public function getFixedNode(INamespaceUseDeclaration $node): Node {
$clauses = $node->getClauses()->getChildrenOfItems();
$clause_count = C\count($clauses);
$unused = $this->getUnusedClauses($node->getKind(), $clauses)
|> Keyset\map($$, $p ==> $p[1]->getUniqueID());
$unused_count = C\count($unused);
if ($clause_count === $unused_count) {
return $node->getFirstTokenx()->getLeading();
}
// Don't create a single-element group use statement
if (
$node is NamespaceGroupUseDeclaration &&
$clause_count === ($unused_count + 1)
) {
$clause = Vec\filter(
$clauses,
$item ==> !C\contains_key($unused, $item->getUniqueID()),
)
|> C\onlyx($$);
$name = $clause->getName();
if ($name is NameToken) {
$name = new QualifiedName(
NodeList::createMaybeEmptyList(
Vec\concat(
$node->getPrefix()->getParts()->getChildren(),
vec[new ListItem($name, null)],
),
),
);
} else {
invariant(
$name is QualifiedName,
'name is not a name or qualified name',
);
$name = new QualifiedName(
NodeList::createMaybeEmptyList(
Vec\concat(
$node->getPrefix()->getParts()->getChildren(),
$name->getParts()->getChildren(),
),
),
);
}
$t = $clause->getFirstTokenx();
$clause = $clause
->withName($name)
->replace($t, $t->withLeading(null));
$fixed = new NamespaceUseDeclaration(
$node->getKeyword(),
$node->getKind(),
NodeList::createMaybeEmptyList(vec[new ListItem($clause, null)]),
$node->getSemicolon(),
);
} else {
$fixed = $node->getClausesx()->getChildren()
|> Vec\filter(
$$,
$child ==> !C\contains_key($unused, $child->getItem()->getUniqueID()),
)
|> $node->withClauses(new NodeList($$));
}
// ->getChildrenByType<ListItem>() can't be used, because ListItem is generic.
$last = C\lastx($fixed->getClauses()->getChildrenOfType(ListItem::class));
$sep = $last->getSeparator();
if ($sep && !Str\contains($sep->getTrailing()->getCode(), "\n")) {
return $fixed->replace($last, $last->withSeparator(null));
}
return $fixed;
}
<<__Memoize>>
private function getUnresolvedReferencedNames(): shape(
'namespaces' => keyset<string>,
'types' => keyset<string>,
'functions' => keyset<string>,
) {
return get_unresolved_referenced_names($this->getAST());
}
}