1/*
2 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16const fs = require('fs');
17const path = require('path');
18const { hasAPINote, getAPINote, overwriteIndexOf, ErrorType, ErrorLevel, FileType, requireTypescriptModule } = require('./utils');
19const { addAPICheckErrorLogs } = require('./compile_info');
20const rules = require('../code_style_rule.json');
21const dictionariesContent = fs.readFileSync(path.resolve(__dirname, '../plugin/dictionaries.txt'), 'utf-8');
22const dictionariesArr = dictionariesContent.split(/[(\r\n)\r\n]+/g);
23const dictionariesSupplementaryContent = fs.readFileSync(path.resolve(__dirname,
24  '../plugin/dictionaries_supplementary.txt'), 'utf-8');
25const dictionariesSupplementaryArr = dictionariesSupplementaryContent.split(/[(\r\n)\r\n]+/g);
26const dictionariesSet = new Set([...dictionariesArr, ...dictionariesSupplementaryArr, ...rules.decorators.customDoc,
27  ...rules.decorators.jsDoc]);
28const ts = requireTypescriptModule();
29
30function checkSpelling(node, sourcefile, fileName) {
31  if (ts.isIdentifier(node) && node.escapedText) {
32    checkWordSpelling(node.escapedText.toString(), node, sourcefile, fileName, FileType.API);
33  } else if (hasAPINote(node)) {
34    const apiNote = getAPINote(node);
35    const words = splitParagraph(apiNote);
36    words.forEach(word => {
37      checkWordSpelling(word, node, sourcefile, fileName, FileType.JSDOC);
38    });
39  }
40}
41exports.checkSpelling = checkSpelling;
42
43function checkWordSpelling(nodeText, node, sourcefile, fileName, type) {
44  const basicWords = splitComplexWords(nodeText);
45  const errorWords = [];
46  const suggest = [];
47  basicWords.forEach(word => {
48    if (!checkBaseWord(word.toLowerCase())) {
49      errorWords.push(word);
50    }
51  });
52  if (errorWords.length !== 0) {
53    errorWords.forEach(errorWord => {
54      const levArr = [];
55      for (let i = 0; i < dictionariesArr.length; i++) {
56        const dictionary = dictionariesArr[i];
57        levArr.push(getLevenshteinValue(errorWord, dictionary));
58      }
59      const minLev = Math.min(...levArr);
60      const indexArr = overwriteIndexOf(minLev, levArr);
61      const MAX_LENGTH = 5;
62      for (let i = 0; i < indexArr.length; i++) {
63        if (i === MAX_LENGTH) {
64          break;
65        }
66        suggest.push(dictionariesArr[indexArr[i]]);
67      }
68    });
69    const errorInfo = `Error words in [${nodeText}]: {${errorWords}}.Do you want to spell it as [${suggest}]?`;
70    addAPICheckErrorLogs(node, sourcefile, fileName, ErrorType.MISSPELL_WORDS, errorInfo, type, ErrorLevel.MIDDLE);
71  }
72}
73
74/**
75 *  check base word
76 * @param {string} word
77 * @return true or false
78**/
79function checkBaseWord(word) {
80  if (/^[a-z][0-9]+$/g.test(word)) {
81    return false;
82  } else if (/^[A-Za-z]+/g.test(word) && !dictionariesSet.has(word.toLowerCase())) {
83    return false;
84  }
85  return true;
86}
87
88function splitComplexWords(complexWord) {
89  let basicWords = [];
90  // splite underlineWord
91  if (hasUnderline(complexWord)) {
92    basicWords = complexWord.split(/(?<!^)\_/g);
93  } else {
94    // splite complexWord
95    if (!/(?<!^)(?=[A-Z])/g.test(complexWord)) {
96      basicWords.push(complexWord);
97    } else {
98      basicWords = complexWord.split(/(?<!^)(?=[A-Z])/g);
99    }
100  }
101  const newBaseWords = [];
102  basicWords.forEach(word => {
103    if (/[0-9]/g.test(word)) {
104      newBaseWords.concat(word.split(/0-9/g));
105    } else {
106      newBaseWords.push(word);
107    };
108  });
109  return newBaseWords;
110}
111exports.splitComplexWords = splitComplexWords;
112
113function hasUnderline(word) {
114  return /(?<!^)\_/g.test(word);
115}
116exports.hasUnderline = hasUnderline;
117
118function splitParagraph(paragraph) {
119  const splitParagraphRegex = /[^\w]/g;
120  const words = paragraph.split(splitParagraphRegex);
121  return words;
122}
123exports.splitParagraph = splitParagraph;
124
125// Levenshtein method
126function getLevenshteinValue(word1, word2) {
127  const word1Len = word1.length;
128  const word2Len = word2.length;
129
130  if (word1Len * word2Len === 0) {
131    return Math.max(word1Len, word2Len);
132  }
133  // create Levenshtein two-dimensional array
134  const levArr = Array.from(new Array(word1Len + 1), () => new Array(word2Len + 1));
135  // set value 0 to Levenshtein two-dimensional array
136  for (let i = 0; i < word1Len + 1; i++) {
137    for (let j = 0; j < word2Len + 1; j++) {
138      levArr[i][j] = 0;
139    }
140  }
141
142  // init Levenshtein two-dimensional array
143  for (let i = 0; i < word1Len + 1; i++) {
144    levArr[i][0] = i;
145  }
146  for (let j = 0; j < word2Len + 1; j++) {
147    levArr[0][j] = j;
148  }
149
150  // calculate levinstein distance
151  for (let i = 1; i < word1Len + 1; i++) {
152    for (let j = 1; j < word2Len + 1; j++) {
153      const countByInsert = levArr[i][j - 1] + 1;
154      const countByDel = levArr[i - 1][j] + 1;
155      const countByReplace = word1.charAt(i - 1) === word2.charAt(j - 1) ?
156        levArr[i - 1][j - 1] : levArr[i - 1][j - 1] + 1;
157      levArr[i][j] = Math.min(countByInsert, countByDel, countByReplace);
158    }
159  }
160
161  return levArr[word1Len][word2Len];
162}
163exports.getLevenshteinValue = getLevenshteinValue;
164