4
* Enforces basic spelling. Spelling inside code is actually pretty hard to
5
* get right without false positives. I take a conservative approach and just
6
* use a blacklisted set of words that are commonly spelled incorrectly.
8
final class ArcanistSpellingLinter extends ArcanistLinter {
10
const LINT_SPELLING_EXACT = 1;
11
const LINT_SPELLING_PARTIAL = 2;
13
private $dictionaries = array();
14
private $exactWordRules = array();
15
private $partialWordRules = array();
17
public function getInfoName() {
18
return pht('Spellchecker');
21
public function getInfoDescription() {
22
return pht('Detects common misspellings of English words.');
25
public function getLinterName() {
29
public function getLinterConfigurationName() {
33
public function getLinterConfigurationOptions() {
35
'spelling.dictionaries' => array(
36
'type' => 'optional list<string>',
37
'help' => pht('Pass in custom dictionaries.'),
41
return $options + parent::getLinterConfigurationOptions();
44
public function setLinterConfigurationValue($key, $value) {
46
case 'spelling.dictionaries':
47
foreach ($value as $dictionary) {
48
$this->loadDictionary($dictionary);
53
return parent::setLinterConfigurationValue($key, $value);
56
public function loadDictionary($path) {
57
$root = $this->getEngine()->getWorkingCopy()->getProjectRoot();
58
$path = Filesystem::resolvePath($path, $root);
60
$dict = phutil_json_decode(Filesystem::readFile($path));
61
PhutilTypeSpec::checkMap(
64
'rules' => 'map<string, map<string, string>>',
66
$rules = $dict['rules'];
68
$this->dictionaries[] = $path;
69
$this->exactWordRules = array_merge(
70
$this->exactWordRules,
71
idx($rules, 'exact', array()));
72
$this->partialWordRules = array_merge(
73
$this->partialWordRules,
74
idx($rules, 'partial', array()));
77
public function addExactWordRule($misspelling, $correction) {
78
$this->exactWordRules = array_merge(
79
$this->exactWordRules,
80
array($misspelling => $correction));
83
public function addPartialWordRule($misspelling, $correction) {
84
$this->partialWordRules = array_merge(
85
$this->partialWordRules,
86
array($misspelling => $correction));
89
public function getLintSeverityMap() {
91
self::LINT_SPELLING_EXACT => ArcanistLintSeverity::SEVERITY_WARNING,
92
self::LINT_SPELLING_PARTIAL => ArcanistLintSeverity::SEVERITY_WARNING,
96
public function getLintNameMap() {
98
self::LINT_SPELLING_EXACT => pht('Possible Spelling Mistake'),
99
self::LINT_SPELLING_PARTIAL => pht('Possible Spelling Mistake'),
103
public function lintPath($path) {
104
// TODO: This is a bit hacky. If no dictionaries were specified, then add
105
// the default dictionary.
106
if (!$this->dictionaries) {
107
$root = dirname(phutil_get_library_root('arcanist'));
108
$this->loadDictionary($root.'/resources/spelling/english.json');
111
foreach ($this->exactWordRules as $misspelling => $correction) {
112
$this->checkExactWord($path, $misspelling, $correction);
115
foreach ($this->partialWordRules as $misspelling => $correction) {
116
$this->checkPartialWord($path, $misspelling, $correction);
120
private function checkExactWord($path, $word, $correction) {
121
$text = $this->getData($path);
123
$num_matches = preg_match_all(
124
'#\b'.preg_quote($word, '#').'\b#i',
127
PREG_OFFSET_CAPTURE);
131
foreach ($matches[0] as $match) {
132
$original = $match[0];
133
$replacement = self::fixLetterCase($correction, $original);
134
$this->raiseLintAtOffset(
136
self::LINT_SPELLING_EXACT,
138
"Possible spelling error. You wrote '%s', but did you mean '%s'?",
146
private function checkPartialWord($path, $word, $correction) {
147
$text = $this->getData($path);
149
while ($pos < strlen($text)) {
150
$next = stripos($text, $word, $pos);
151
if ($next === false) {
154
$original = substr($text, $next, strlen($word));
155
$replacement = self::fixLetterCase($correction, $original);
156
$this->raiseLintAtOffset(
158
self::LINT_SPELLING_PARTIAL,
160
"Possible spelling error. You wrote '%s', but did you mean '%s'?",
169
public static function fixLetterCase($string, $case) {
171
case strtolower($case):
172
return strtolower($string);
173
case strtoupper($case):
174
return strtoupper($string);
175
case ucwords(strtolower($case)):
176
return ucwords(strtolower($string));