@quintaaa/eslint-plugin-starlims
Version:
Eslint plugin to parse and lint starlims form code successfully
216 lines (200 loc) • 9.9 kB
JavaScript
const sourceRegex = /^[A-Za-z_]+(\.[A-Za-z_]+){1,2}$/;
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Check links syntax such as ClientScripts, ServerScripts, DataSources, etc.',
recommended: true,
},
},
create(context) {
const sourceCode = context.getSourceCode();
const includeComment = /^\s*#include/;
const includeEndingWithSemicolon = /^#include\s+.*;\s*$/gm;
const validIncludeComment =
/^#include\s+["<](?<cs>\w+\.\w+)[">]\s*\r?$/;
return {
Program() {
for (const comment of sourceCode.getAllComments()) {
// Only trigger on include statements
if (!includeComment.test(comment.value)) continue;
if (validIncludeComment.test(comment.value)) continue; // If the source string is valid, don't report it
// If the include statement ends with a semicolon, report it
if (includeEndingWithSemicolon.test(comment.value))
context.report({
node: comment,
message:
'Your include statement seems to be wrong, please remove the semicolon at the end',
});
else
context.report({
node: comment,
message:
'Your include statement seems to be wrong, please use `#include "Category.ScriptName"`',
});
}
},
CallExpression(node) {
// Only trigger on MemberExpressions (e.g. lims.GetDataSource)
if (node.callee.type !== 'MemberExpression') return;
// Only trigger on linkFunctions (e.g. lims.GetDataSource, lims.CallServer, etc.)
const { object, property } = node.callee;
if (
object.name !== 'lims' ||
!linkFunctions.includes(property.name)
)
return;
const { arguments: funcArgs } = node;
// Check if the function has the right number of arguments
if (funcArgs.length === 0) {
return context.report({
node,
message:
"This function must have at least the source argument (e.g. 'Category.Name')",
});
}
// Check if the first argument is a valid source string
const source = funcArgs[0];
if (source.type === 'Identifier') {
const invalidRefs = getInvalidRefs(
source.name,
sourceCode.getScope(node)
);
invalidRefs.forEach((invalidRef) => {
context.report({
node: invalidRef,
message:
"This variable used as a source string must always be a valid source string (e.g. 'Category.Name')" +
(invalidRef.type === 'Identifier'
? ` - variable ${invalidRef.name} is not always a valid source string`
: ''),
});
});
if (invalidRefs.length > 0)
context.report({
node: source,
message:
"This variable must be a valid source string (e.g. 'Category.Name'), please check the errors on this variable assignments",
});
} else if (
source.type !== 'Literal' ||
!sourceRegex.test(source?.value)
) {
// If the first argument is not a variable, it must be the source string literal
context.report({
node: source,
message:
"The first argument of this function must be a source string (e.g. 'Category.Name')",
});
}
const params = funcArgs[1];
if (!params) return; // If there's no second argument, everything is fine (e.g. lims.GetDataSource('Category.Name'))
// If the second argument is an array, everything is fine
// If it is a function call, we do not check it (e.g. lims.GetDataSource('Category.Name', getParams()))
if (['ArrayExpression', 'CallExpression'].includes(params.type))
return;
// If the second argument is a variable, check if it's an array
if (params.type === 'Identifier') {
const nonArrayAssignments = getNonArrayAssignments(
params.name,
sourceCode.getScope(node)
);
nonArrayAssignments.forEach((assignment) => {
context.report({
node: assignment,
message:
'This variable used as a parameters array must be an array, please check the errors on this variable assignments' +
(assignment.type === 'Identifier'
? ` - variable ${assignment.name} is not always array`
: ''),
});
});
if (nonArrayAssignments.length > 0)
context.report({
node: params,
message:
'The parameter argument must be an array, please check the errors on this variable assignments',
});
} else {
context.report({
node: params,
message: 'Parameters must be an array',
});
}
},
};
},
};
const linkFunctions = [
'GetDataSource',
'CallServer',
'CallServerAsync',
'GetDataSet',
'GetDataSetAsync',
'GetData',
'GetDataAsync',
'GetBinarySource',
'GetFormSource',
'GetReportSource',
];
const getNonArrayAssignments = (variableName, scope) => {
let varScope = scope;
let variable = null;
do {
// Search for the variable in the scope chain going up
variable = varScope.set.get(variableName);
varScope = varScope.upper;
} while (!variable && varScope !== null);
if (!variable) return []; // Variable is not defined, handled by undefined-variable rule
const references = variable.references;
return references // Filter all the references that are not valid parameters, leave the ones that are array or too complex to check
.filter((ref) => {
const type = ref.identifier.parent.type;
if (!['VariableDeclarator', 'AssignmentExpression'].includes(type))
return false; // Only check variable assignments
const writeExpr = ref.writeExpr;
if (!writeExpr) return false; // Only check variables that are being assigned, not the ones that are being read
// If the variable is referring to another variable, check if that variable is a valid source string
if (writeExpr.type === 'Identifier')
return getNonArrayAssignments(writeExpr.name, scope).length > 0;
// If the variable is a CallExpression, don't check further
if (
writeExpr.type === 'CallExpression' ||
writeExpr.type === 'MemberExpression'
)
return false;
// If the variable is not an array, it's not valid
return writeExpr.type !== 'ArrayExpression';
})
.flat()
.map((ref) => ref.writeExpr);
};
const getInvalidRefs = (variableName, scope) => {
let varScope = scope;
let variable = null;
do {
// Search for the variable in the scope chain going up
variable = varScope.set.get(variableName);
varScope = varScope.upper;
} while (!variable && varScope !== null);
if (!variable) return []; // Variable is not defined, handled by undefined-variable rule
const references = variable.references;
return references
.filter((ref) => {
const type = ref.identifier.parent.type;
if (!['VariableDeclarator', 'AssignmentExpression'].includes(type))
return false; // Only check variable assignments
const writeExpr = ref.writeExpr;
if (!writeExpr) return false; // Only check variables that are being assigned, not the ones that are being read
// If the variable is referring to another variable, check if that variable is a valid source string
if (writeExpr.type === 'Identifier')
return getInvalidRefs(writeExpr.name, scope).length > 0;
// Do not check if the variable is too complex to check
if (!writeExpr.value) return false;
// If the variable is not a valid source string, it's not valid
return !sourceRegex.test(writeExpr.value);
})
.flat()
.map((ref) => ref.writeExpr);
};