Skip to content

Commit

Permalink
chore: extend auto-unsigned rule to exclusive predefined options
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Sep 28, 2023
1 parent ab44028 commit 1f40d66
Showing 1 changed file with 131 additions and 62 deletions.
193 changes: 131 additions & 62 deletions packages/eslint-plugin/src/rules/auto-unsigned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,56 @@ export const autoUnsigned: JSONCRule.RuleModule = {
if (!context.parserServices.isJSON) {
return {};
}

function suggestUnsigned(
parent: AST.JSONObjectExpression,
valueSizeNode: AST.JSONProperty,
valueSize: number,
minValue: number,
maxValue: number,
minLimit: number,
maxLimit: number,
) {
// Find the property after which we should insert the unsigned property
const unsignedIndex = paramInfoPropertyOrder.indexOf(
"unsigned",
);
const insertAfter = parent.properties.findLast((p) =>
p.key.type === "JSONLiteral"
&& typeof p.key.value === "string"
&& paramInfoPropertyOrder.indexOf(p.key.value)
< unsignedIndex
);
context.report({
loc: valueSizeNode.loc,
messageId: "incompatible-size",
data: {
minValue: minValue.toString(),
maxValue: maxValue.toString(),
valueSize: valueSize.toString(),
sizeMin: minLimit.toString(),
sizeMax: maxLimit.toString(),
},
suggest: [
{
messageId: "convert-to-unsigned",
fix: insertAfter
? insertAfterJSONProperty(
context,
insertAfter,
`"unsigned": true,`,
{ insertComma: true },
)
: insertBeforeJSONProperty(
context,
parent.properties[0],
`"unsigned": true,`,
),
},
],
});
}

return {
// Ensure `unsigned` is only used when necessary and not used when not
"JSONProperty[key.value='paramInformation'] > JSONArrayExpression > JSONObjectExpression"(
Expand All @@ -38,31 +88,67 @@ export const autoUnsigned: JSONCRule.RuleModule = {
// TODO: Properly support partial parameters
if (valueBitMask) return;

// We're also not looking at options with allowManualEntry = false yet
// To determine the min or max value, we look at options if allowManualEntry is false
let minValueNode: AST.JSONProperty | undefined;
let maxValueNode: AST.JSONProperty | undefined;
let minValue: number;
let maxValue: number;
let isUsingOptions: boolean;

const allowManualEntry =
getJSONBoolean(node, "allowManualEntry")?.value !== false;
const hasOptions = node.properties.some((p) =>
p.key.type === "JSONLiteral"
&& p.key.value === "options"
&& p.value.type === "JSONArrayExpression"
&& p.value.elements.length > 0
);
const options =
(node.properties.find((p) =>
p.key.type === "JSONLiteral"
&& p.key.value === "options"
&& p.value.type === "JSONArrayExpression"
&& p.value.elements.length > 0
)?.value as AST.JSONArrayExpression | undefined)
?.elements
?.filter((e): e is AST.JSONObjectExpression => !!e);

// TODO: Look at options
if (!allowManualEntry && hasOptions) return;
if (!allowManualEntry && !!options) {
// Deduce min/max value from options
const sortedOptions = options
.map((o) => {
const valueProp = getJSONNumber(o, "value");
const label = getJSONString(o, "label")?.value;
if (label == undefined || valueProp == undefined) {
return;
}
return {
valueNode: valueProp.node,
value: valueProp.value,
label,
};
}).filter(Boolean)
.sort((a, b) => a!.value - b!.value);

if (sortedOptions.length === 0) return;

minValueNode = sortedOptions[0]!.valueNode;
minValue = sortedOptions[0]!.value;
maxValueNode = sortedOptions.at(-1)!.valueNode;
maxValue = sortedOptions.at(-1)!.value;
isUsingOptions = true;
} else {
// Otherwise consider min/max value
const minValueProperty = getJSONNumber(node, "minValue");
if (!minValueProperty) return;
minValueNode = minValueProperty.node;
minValue = minValueProperty.value;

const maxValueProperty = getJSONNumber(node, "maxValue");
if (!maxValueProperty) return;
maxValueNode = maxValueProperty.node;
maxValue = maxValueProperty.value;
isUsingOptions = false;
}

const valueSizeProperty = getJSONNumber(node, "valueSize");
if (!valueSizeProperty) return;
const { value: valueSize } = valueSizeProperty;

const minValueProperty = getJSONNumber(node, "minValue");
if (!minValueProperty) return;
const { value: minValue } = minValueProperty;

const maxValueProperty = getJSONNumber(node, "maxValue");
if (!maxValueProperty) return;
const { value: maxValue } = maxValueProperty;

const unsignedProperty = getJSONBoolean(node, "unsigned");
const isUnsigned = !!unsignedProperty?.value;

Expand Down Expand Up @@ -92,49 +178,22 @@ export const autoUnsigned: JSONCRule.RuleModule = {

if (!isUnsigned && !fitsSignedLimits) {
if (fitsUnsignedLimits) {
// Find the property after which we should insert the unsigned property
const unsignedIndex = paramInfoPropertyOrder.indexOf(
"unsigned",
);
const insertAfter = node.properties.findLast((p) =>
p.key.type === "JSONLiteral"
&& typeof p.key.value === "string"
&& paramInfoPropertyOrder.indexOf(p.key.value)
< unsignedIndex
suggestUnsigned(
node,
valueSizeProperty.node,
valueSize,
minValue,
maxValue,
limits.min,
limits.max,
);
context.report({
loc: valueSizeProperty.node.loc,
messageId: "incompatible-size",
data: {
minValue: minValue.toString(),
maxValue: maxValue.toString(),
valueSize: valueSize.toString(),
sizeMin: limits.min.toString(),
sizeMax: limits.max.toString(),
},
suggest: [
{
messageId: "convert-to-unsigned",
fix: insertAfter
? insertAfterJSONProperty(
context,
insertAfter,
`"unsigned": true,`,
{ insertComma: true },
)
: insertBeforeJSONProperty(
context,
node.properties[0],
`"unsigned": true,`,
),
},
],
});
} else {
if (minValue < limits.min) {
context.report({
loc: minValueProperty.node.loc,
messageId: "incompatible-min-value",
loc: minValueNode.loc,
messageId: isUsingOptions
? "incompatible-min-option-value"
: "incompatible-min-value",
data: {
minValue: minValue.toString(),
valueSize: valueSize.toString(),
Expand All @@ -144,8 +203,10 @@ export const autoUnsigned: JSONCRule.RuleModule = {
}
if (maxValue > limits.max) {
context.report({
loc: maxValueProperty.node.loc,
messageId: "incompatible-max-value",
loc: maxValueNode.loc,
messageId: isUsingOptions
? "incompatible-max-option-value"
: "incompatible-max-value",
data: {
maxValue: maxValue.toString(),
valueSize: valueSize.toString(),
Expand All @@ -157,8 +218,10 @@ export const autoUnsigned: JSONCRule.RuleModule = {
} else if (isUnsigned && !fitsUnsignedLimits) {
if (minValue < unsignedLimits.min) {
context.report({
loc: minValueProperty.node.loc,
messageId: "incompatible-min-value",
loc: minValueNode.loc,
messageId: isUsingOptions
? "incompatible-min-option-value"
: "incompatible-min-value",
data: {
minValue: minValue.toString(),
valueSize: valueSize.toString(),
Expand All @@ -168,8 +231,10 @@ export const autoUnsigned: JSONCRule.RuleModule = {
}
if (maxValue > unsignedLimits.max) {
context.report({
loc: maxValueProperty.node.loc,
messageId: "incompatible-max-value",
loc: maxValueNode.loc,
messageId: isUsingOptions
? "incompatible-max-option-value"
: "incompatible-max-value",
data: {
maxValue: maxValue.toString(),
valueSize: valueSize.toString(),
Expand Down Expand Up @@ -202,6 +267,10 @@ export const autoUnsigned: JSONCRule.RuleModule = {
"invalid-value-size": "Value size {{valueSize}} is invalid!",
"incompatible-size":
"The defined value range {{minValue}}...{{maxValue}} is incompatible with valueSize {{valueSize}} ({{sizeMin}}...{{sizeMax}})",
"incompatible-min-option-value":
"Option value {{minValue}} is incompatible with valueSize {{valueSize}} (min = {{sizeMin}})",
"incompatible-max-option-value":
"Option value {{maxValue}} is incompatible with valueSize {{valueSize}} (max = {{sizeMax}})",
"incompatible-min-value":
"minValue {{minValue}} is incompatible with valueSize {{valueSize}} (min = {{sizeMin}})",
"incompatible-max-value":
Expand Down

0 comments on commit 1f40d66

Please sign in to comment.