tools/eslint/rules/valid-constructors.js (132 lines of code) (raw):
/*
RULES
1. Private constructors don't get linted
2. Must be 1, 2 or 3 parameters
3. First parameter must be called scope
4. First parameter must be of type GuStack
5. If 2 parameters:
- The second parameter must be called props
- The second parameter must be a custom type
6. If 3 parameters:
- The second parameter must be called id
- The second parameter must be of type string
- The third parameter must be called props
- The third parameter must be a custom type
7. Only the third parameter can be optional or have a default value
See `valid-constructors.test.ts` and `npm run test:custom-lint-rule`
TODO: If all values in third type are optional then parameter should be optional
*/
const NO_LINT_ERRORS = null;
const lintParameter = (param, node, context, { name, type, allowOptional, allowDefault, position }) => {
const hasDefault = param.type === "AssignmentPattern";
if (hasDefault && !allowDefault) {
return context.report({
node,
loc: param.loc,
message: `The ${position} parameter in a constructor cannot have a default`,
});
}
const isOptional = param.optional;
if (isOptional && !allowOptional) {
return context.report({
node,
loc: param.loc,
message: `The ${position} parameter in a constructor cannot be optional`,
});
}
const currentName = param.name ?? param.left?.name ?? param.argument.name;
if (currentName !== name) {
return context.report({
node,
loc: param.loc,
message: `The ${position} parameter in a constructor must be called ${name} (currently ${currentName})`,
});
}
const tsType = param.typeAnnotation?.typeAnnotation.type ?? param.left.typeAnnotation.typeAnnotation.type;
const customType =
param.typeAnnotation?.typeAnnotation.typeName?.name ?? param.left?.typeAnnotation.typeAnnotation.typeName.name;
if (type instanceof RegExp) {
if (!type.test(customType)) {
return context.report({
node,
loc: param.typeAnnotation.loc,
message: `The ${position} parameter in a constructor must match ${type.toString()} (currently ${customType})`,
});
}
} else {
const currentType = type.startsWith("TS") ? tsType : customType;
if (currentType !== type) {
return context.report({
node,
loc: param.typeAnnotation.loc,
message: `The ${position} parameter in a constructor must be of type ${type} (currently ${currentType})`,
});
}
}
};
const scopeParamSpec = {
name: "scope",
type: "GuStack",
allowOptional: false,
allowDefault: false,
position: "first",
};
const idParamSpec = {
name: "id",
type: "TSStringKeyword",
allowOptional: false,
allowDefault: false,
position: "second",
};
const propsAsSecondParamSpec = {
name: "props",
type: "TSTypeReference",
allowOptional: false,
allowDefault: false,
position: "second",
};
const propsAsThirdParamSpec = {
name: "props",
type: "TSTypeReference",
allowOptional: true,
allowDefault: true,
position: "third",
};
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "ensure constructors conform with agreed pattern",
category: "Best Practices",
url: "https://github.com/guardian/cdk/blob/main/docs/architecture-decision-records/002-component-constuctors.md",
},
schema: [],
},
create(context) {
return {
MethodDefinition(node) {
if (node.kind !== "constructor") return NO_LINT_ERRORS;
if (node.accessibility === "private") return NO_LINT_ERRORS;
const params = node.value.params;
if (!Array.isArray(params)) {
return context.report({
node,
message: "Constructors must take at least one parameter",
loc: params.loc,
});
}
switch (params.length) {
case 1: {
const [scope] = params;
return lintParameter(scope, node, context, scopeParamSpec) ?? NO_LINT_ERRORS;
}
case 2: {
const [scope, props] = params;
return (
lintParameter(scope, node, context, scopeParamSpec) ??
lintParameter(props, node, context, propsAsSecondParamSpec) ??
NO_LINT_ERRORS
);
}
case 3: {
const [scope, id, props] = params;
return (
lintParameter(scope, node, context, scopeParamSpec) ??
lintParameter(id, node, context, idParamSpec) ??
lintParameter(props, node, context, propsAsThirdParamSpec) ??
NO_LINT_ERRORS
);
}
default:
return context.report({
node,
message: `Constructors can take a maximum of three parameters - there are ${params.length} params here!`,
loc: params.loc,
});
}
},
};
},
};