rflint
Version:
A tool to check style and conventions of Robot Framework project.
414 lines (386 loc) • 18.1 kB
JavaScript
const Path = require('path');
const fs = require('fs');
const Parser = require('./parser');
const program = require('commander');
const [node, path, ...argv] = process.argv;
const consoleJson = new Array();
const Pwd = process.cwd();
require('colors');
/// 参数处理
const pkg = JSON.parse(fs.readFileSync(`${__dirname}/../package.json`, 'utf8'));
program
.version(pkg.version, '-v, --version')
.parse(process.argv);
/// 启动检测
run();
/// 递归查询robot文件
function readFileList(dir, fileList = []) {
const files = fs.readdirSync(dir);
files.forEach((item) => {
var fullpath = Path.join(dir, item);
const stat = fs.statSync(fullpath);
if (stat.isDirectory()) {
readFileList(fullpath, fileList);
} else if (fullpath.endsWith('.robot')) {
fileList.push(fullpath);
}
});
return fileList;
}
/// 查找robot文件
function searchFiles(filelist = []) {
if (filelist.length>0) {
filelist.forEach((file) => {
/// 判断文件是.robot后缀
if (file.endsWith('.robot')) {
var fileName = file.split('/');
console.log("⚙ Find a robot file: " + fileName.pop().yellow + ", start lint...");
lintFile(file);
}
});
console.log("🗞 Total files: " + filelist.length + "\n");
console.log('✅ Lint done! There is your report: ' + '\n 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠 🦠');
/// 检测完后打印违规信息
console.log(JSON.stringify(consoleJson, null, "\t").green);
}
}
function run() {
console.log("🚀 PreLint...");
console.log("👺 Current Directory: " + Pwd);
console.log(">>>>" + process.argv[2]);
var fileList = [];
if (process.argv[2] != undefined) {
var filepath = Pwd + "/" + process.argv[2];
fileList = [filepath];
} else {
fileList = readFileList(Pwd, fileList);
}
if (fileList.length == 0) {
console.log('❌ 没有找到相应的文件,请确认您的当前目录是否是在项目根目录!');
process.exit();
}
searchFiles(fileList);
}
/// 对文件进行lint检测
function lintFile(file) {
/// 解析关键字
var parser = new Parser();
parser.parserFile(file);
/// 检测是否有Documentation"
fileHasDocumentation(file, parser.tables)
/// 不重名 (Error)
noSameName(file, parser.tables)
/// for循环内关键字是否有反斜杠标识 (Error)
checkForLoop(file, parser.tables)
/// if语句下面是否是"..."开头
checkIf(file, parser.tables)
/// resource和Test SetUp之间应该空一行
checkResourceAndTestSetUp(file, parser.tables)
/// keyword里面不能包含testCase
keywordShouldNotContainTestCase(file, parser.tables)
/// 字符串判断引号和单引号成对出现
compareStringNeedSingleQuote(file, parser.tables)
/// 字符串定义和比较使用双引号,单引号报警告、
stringUseDoubleQuoteSigleQuoteWarning(file, parser.tables)
/// 传参的时候把参数的参数名补上
completeParameters(file, parser.tables)
}
/// 检测是否写了Documentation
function fileHasDocumentation(file, tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
var isHasDocumentation = false;
if (table.name == 'Settings') {
for (let j = 0; j < table.rows.length; j++) {
let row = table.rows[j];
/// 有documentation
if (row.cells.length > 0) {
if (row.cells[0].text == 'Documentation') {
/// 只有'Documentation'关键字,没有补全信息
isHasDocumentation = true;
if (row.cells.length < 2) {
let outputInfo = constructOutPutJson(row.cells[0].lineNumber,file,table.rows[j].lineNumber,'Settings里面有Documentation,但并没有补全文件信息', 'Warning', 'Documentation')
consoleJson.push(outputInfo);
}
}
}
if (!isHasDocumentation && (j == table.rows.length -1)) {
/// 如果没有,构建违规信息
let outputInfo = constructOutPutJson(0, file, row.lineNumber, 'Settings里面没有Documentation,为了方便生成文档,建议加上Documentation', 'Warning', 'Documentation')
consoleJson.push(outputInfo);
}
}
}
}
}
/// Variable、Keywords、TestCase文件内不能重名
function noSameName(file, tables) {
/// 存放keyWords、testCase、Variable的row数组
var keywordsList = new Array();
for (let i = 0; i < tables.length; i++) {
let table = tables[i]
/// settings不检查
if (table.name != 'Settings') {
for (let j = 0; j < table.rows.length; j++) {
var row = table.rows[j];
let cells = row.cells;
/// 如果是关键字 加入数组
if (cells.length > 0) {
let firstCell = cells[0];
/// 如果是顶格写的并且不以'...'、'#'开头的cell,则判定为一个Variable/Keywords/TestCase
if ((firstCell.lineNumber == 0) && (String(firstCell.text)[0] != '.') && (String(firstCell.text)[0] != '#') && (String(firstCell.text) != 'undefined')) {
if (keywordsList.length == 0) {
keywordsList.push(row);
} else {
/// 判断是否有同名的关键字
let currentKeyWordList = keywordsList.slice()
for (let m = 0; m < currentKeyWordList.length; m++) {
let keyWordRow = currentKeyWordList[m];
let keyword = keyWordRow.cells[0].text
/// 如果有同名的关键字
if (String(keyword) == String(firstCell.text)) {
/// 构建错误信息
let outputInfo = constructOutPutJson(keyWordRow.cells[0].lineNumber, file, keyWordRow.lineNumber + ',' + row.lineNumber, '文件内有同名的Variable、Keywords、TestCase: ' + keyword, 'Error', 'Same Name')
consoleJson.push(outputInfo);
} else if (m == (currentKeyWordList.length-1)){
/// 不同名则加入数组
keywordsList.push(row);
}
}
}
}
}
}
}
}
}
/// 检测FOR循环内关键字用“\”标识
function checkForLoop(file,tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
for (let j = 0; j < table.rows.length; j++) {
let cells = table.rows[j].cells;
if (cells.length > 0) {
if (cells[0].text == ':FOR') {
let index = j + 1;
/// 过滤注释
for (let m = (j+1); m < table.rows.length; m++) {
let row = table.rows[m];
if (row.cells.length > 0) {
if (String(row.cells[0].text).indexOf('#') != 0) {
index = m;
break;
}
}
}
let nextCells = table.rows[index].cells;
/// 检测for循环首行第一个cell必须为‘\’, 过滤注释
if (nextCells[0].text != '\\') {
let output = constructOutPutJson(cells[0].lineNumber, file, table.rows[j+1].lineNumber, 'FOR循环内关键字用反斜杠换行', 'Error', 'For Loop')
consoleJson.push(output);
}
}
}
}
}
}
/// 检测if后面的语句是否是"..."开头
function checkIf(file, tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
for (let j = 0; j < table.rows.length; j++) {
let cells = table.rows[j].cells;
if (cells.length > 0) {
if (cells[0].text == 'Run Keyword If') {
let nextCells = table.rows[j+1].cells
/// 检测run keyword if下一个row的第一个cell应该为'...'
if (nextCells.length == 0) {
let output = constructOutPutJson(cells[0].lineNumber, file,table.rows[j+1].lineNumber, 'Run Keyword If条件语句之后的条件代码尽量另起一行,并用\'...\'换行 ++', 'Warning', 'Run Keyword If')
consoleJson.push(output);
} else if (nextCells[0].text != '...') {
let str = nextCells[0].text;
nextCells.forEach((item) => {
str += item.text;
});
let output = constructOutPutJson(cells[0].lineNumber, file,table.rows[j+1].lineNumber, 'Run Keyword If条件语句之后的条件代码尽量另起一行,并用\'...\'换行 --- ' + nextCells[0].text + '\n' + str, 'Warning', 'Run Keyword If')
consoleJson.push(output);
}
}
}
}
}
}
/// resource和Test SetUp之间应该空一行
function checkResourceAndTestSetUp(file, tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
if (table.name == 'Settings') {
for (let j = 0; j < table.rows.length; j++) {
let cells = table.rows[j].cells;
/// 如果不是空行
if (cells.length > 0) {
/// 如果有"Test SetUp", 检测前面是否有空行
if (cells[0].text == 'Test Setup') {
if (table.rows[j-1].cells.length == 0) {
// console.log('====>Test SetUp前有空行')
} else {
let output = constructOutPutJson(cells[0].lineNumber, file, table.rows[j].lineNumber, 'Test Setup与前一个关键字之间应该有空行', 'Warning', 'Line Space')
consoleJson.push(output)
// console.log('====>Test SetUp前没有空行')
}
}
}
}
}
}
}
/// keyword里面不能包含testCase,(暂时通过比较两个关键字行数)
function keywordShouldNotContainTestCase(file, tables) {
let hasKeyWordsTable = false;
let hasTestCaseTable = false;
let keyWordsTable = tables[0];
let TestCaseTable = tables[0];
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
let tableName = table.name;
if (tableName == 'Keywords') {
hasKeyWordsTable = true;
keyWordsTable = table
}
if (tableName == 'Test Case') {
hasTestCaseTable = true;
TestCaseTable = table
}
}
/// 如果同时都有的话,报告错误
if (hasKeyWordsTable && hasTestCaseTable && (parseInt(keyWordsTable.lineNumber) < parseInt(TestCaseTable.lineNumber))) {
let outputInfo = constructOutPutJson(0, file, keyWordsTable.lineNumber + ',' + TestCaseTable.lineNumber, 'keyWords里面不应该有Test Case', 'Warning', 'KeyWordsAndTestCase')
consoleJson.push(outputInfo);
}
}
/// 字符串判断引号和单引号成对出现
/*
"${result['data']['carType']}" == "2"
'plateNo' in ${result['data']['orderCarVO']}
${length} == 1
${first_follow_period} > 7200
'${flag}'!='FAIL'
*/
function compareStringNeedSingleQuote(file, tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
for (let j = 0; j < table.rows.length; j++) {
let row = table.rows[j]
for (let m = 0; m < row.cells.length; m++) {
let cell = row.cells[m];
/// 如果有条件判断
if (String(cell.text) == 'Run Keyword If') {
/// 如果是最后一个cell,不进行解析
if (m == (row.cells.length - 1)) {
continue
}
let nextText = row.cells[m+1].text;
let equalReg = RegExp(/==/)
let unequalReg = RegExp(/!=/)
if (equalReg.test(String(nextText)) || unequalReg.test(String(nextText))) {
/// 如果是开头有单引号, 报字符串单引号警告
if (String(nextText)[0] == '\'') {
/// 结尾如果不是单引号,报告错误
if (String(nextText)[(String(nextText).length -1)] != '\'') {
let outputInfo = constructOutPutJson(row.cells[m+1].lineNumber, file,row.lineNumber, '字符判断两边都要打上单引号', 'Warning', 'Quote');
consoleJson.push(outputInfo);
}
} else if (String(nextText)[0] == '\"') { /// 如果开头有双引号
/// 结尾如果不是双引号,报告错误
if (String(nextText)[(String(nextText).length -1)] != '\"') {
let outputInfo = constructOutPutJson(row.cells[m+1].lineNumber, file, row.lineNumber, '字符判断两边都要打上双引号', 'Warning', 'Quote');
consoleJson.push(outputInfo);
}
} else if (String(nextText)[(String(nextText).length - 1)] == '\'') { /// 如果结尾是单引号
/// 如果开头不是单引号
if (String(nextText)[0] != '\'') {
let outputInfo = constructOutPutJson(row.cells[m+1].lineNumber, file, row.lineNumber, '字符判断两边都要打上单引号', 'Warning', 'Quote');
consoleJson.push(outputInfo);
}
} else if (String(nextText)[(String(nextText).length - 1)] == '\"') { /// 如果结尾是双引号
if (String(nextText)[0] != '\"') {
let outputInfo = constructOutPutJson(row.cells[m+1].lineNumber, file,row.lineNumber, '字符判断两边都要打上双引号', 'Warning', 'Quote');
consoleJson.push(outputInfo);
}
}
}
}
}
}
}
}
/// 字符串使用双引号单引号报警告
function stringUseDoubleQuoteSigleQuoteWarning(file, tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
for (let j = 0; j < table.rows.length; j++) {
let row = table.rows[j];
let cells = row.cells;
for (let m = 0; m < cells.length; m++) {
let cell = cells[m]
/// 如果是单引号开头或结尾
if (cell.text[0] == '\'' || cell.text[cell.text.length - 1] == '\'') {
let outPutInfo = constructOutPutJson(cell.lineNumber, file, row.lineNumber, '字符串定义和判断尽量用双引号', 'Warning', 'Quote')
consoleJson.push(outPutInfo)
}
}
}
}
}
/// 传参的时候把参数的参数名补上
function completeParameters(file, tables) {
for (let i = 0; i < tables.length; i++) {
let table = tables[i];
for (let j = 0; j < table.rows.length; j++) {
let row = table.rows[j];
let cells = row.cells;
for (let m = 0; m < cells.length; m++) {
let cell = cells[m];
/// 不是关键字或者suite名
if (cell.lineNumber != 0) {
/// 查找语句里面是否带有中文的
let cellText = cell.text;
let chinessReg = RegExp(/(^[a-z]+|^[\u4e00-\u9fa5]+)[\u4e00-\u9fa5]+/);
if (chinessReg.test(cellText)) {
/// 取下一个cell
if (m != (cells.length - 1)) {
let nextCell = cells[m+1];
let patamserList = nextCell.text.split('=');
/// 如果=切分后只有一个元素则报警告
if (patamserList.length < 2) {
let outPutInfo = constructOutPutJson(nextCell.lineNumber, file, row.lineNumber, '传参的时候没有把参数名补齐。关键字: ' + cellText, 'Warning', 'ParameterName')
consoleJson.push(outPutInfo)
}
}
}
}
}
}
}
}
/**
* 违规信息字典
* 1.character: 违规字符地址;
* 2.file: 违规文件路径;
* 3.line: 违规行号;
* 4.reason: 违规原因;
* 5.rule_id: 违规规则id;
* 6.severity: 违规安全策略;
* 7.type: 违规type
*/
function constructOutPutJson(character, file, line, reason, severity, type) {
var dic= { "character": character,
"file": file,
"line": line,
"reason": reason,
"severity": severity,
"type": type
}
return dic;
}