mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-26 11:34:14 +00:00
Split up Maki eslint rules
This commit is contained in:
parent
30ed04c3f7
commit
a3d6615f3f
6 changed files with 329 additions and 261 deletions
|
|
@ -1,263 +1,9 @@
|
|||
const { objects } = require("./modern/src/maki-interpreter/objects");
|
||||
|
||||
const classNameMappings = {
|
||||
Object: "MakiObject",
|
||||
"@{00000000-0000-0000-0000-000000000000}@": null,
|
||||
};
|
||||
|
||||
function normalizeClassName(className) {
|
||||
const normalized = classNameMappings[className];
|
||||
return normalized === undefined ? className : normalized;
|
||||
}
|
||||
|
||||
const objectsByName = {};
|
||||
for (const value of Object.values(objects)) {
|
||||
objectsByName[normalizeClassName(value.name)] = value;
|
||||
}
|
||||
|
||||
const TYPE_MAP = {
|
||||
// This might be wrong. Maybe it really is an empty string? Or Null?
|
||||
"": {
|
||||
typeScriptName: "TSVoidKeyword",
|
||||
stringRepresentation: "void",
|
||||
},
|
||||
string: {
|
||||
typeScriptName: "TSStringKeyword",
|
||||
stringRepresentation: "string",
|
||||
},
|
||||
double: {
|
||||
typeScriptName: "TSNumberKeyword",
|
||||
stringRepresentation: "number",
|
||||
},
|
||||
int: {
|
||||
typeScriptName: "TSNumberKeyword",
|
||||
stringRepresentation: "number",
|
||||
},
|
||||
boolean: {
|
||||
typeScriptName: "TSBooleanKeyword",
|
||||
stringRepresentation: "boolean",
|
||||
},
|
||||
float: {
|
||||
typeScriptName: "TSNumberKeyword",
|
||||
stringRepresentation: "number",
|
||||
},
|
||||
any: {
|
||||
typeScriptName: "TSAnyKeyword",
|
||||
stringRepresentation: "any",
|
||||
},
|
||||
};
|
||||
const makiClassRule = require("./modern/eslint/maki-class");
|
||||
const makiMissingMethods = require("./modern/eslint/maki-missing-methods");
|
||||
const makiMethodTypes = require("./modern/eslint/maki-method-types");
|
||||
|
||||
module.exports = {
|
||||
"maki-methods": {
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Ensure Maki objects match std.mi",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
create: function(context) {
|
||||
let currentObject = null;
|
||||
return {
|
||||
"ClassDeclaration:exit": function() {
|
||||
currentObject = null;
|
||||
},
|
||||
ClassDeclaration: function(node) {
|
||||
const className = node.id.name;
|
||||
// https://github.com/captbaritone/webamp/pull/828#issuecomment-518023519
|
||||
if (className.startsWith("Js")) {
|
||||
return;
|
||||
}
|
||||
currentObject = objectsByName[className];
|
||||
if (currentObject == null) {
|
||||
context.report({
|
||||
node: node.id,
|
||||
message: `Unknown Maki Class \`${className}\`.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const expectedParentClassName = normalizeClassName(
|
||||
currentObject.parent
|
||||
);
|
||||
if (expectedParentClassName == null) {
|
||||
if (node.superClass !== null) {
|
||||
context.report({
|
||||
node: node.id,
|
||||
message: `Unexpected parent class for \`${className}\`. \`${className}\` should not extend any class.`,
|
||||
});
|
||||
}
|
||||
// This is probably MakiObject which does not inherit from anything
|
||||
return;
|
||||
}
|
||||
const parentClassName = node.superClass.name;
|
||||
if (parentClassName !== expectedParentClassName) {
|
||||
context.report({
|
||||
node: node.superClass,
|
||||
message: `Incorrect parent class \`${parentClassName}\`. Expected \`${expectedParentClassName}\`.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
ClassBody: function(node) {
|
||||
if (currentObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const implementedMethodNames = new Set(
|
||||
node.body
|
||||
.filter(prop => prop.type === "MethodDefinition")
|
||||
.map(method => method.key.name)
|
||||
);
|
||||
|
||||
currentObject.functions.forEach(func => {
|
||||
const methodName = func.name.toLowerCase();
|
||||
if (implementedMethodNames.has(methodName)) {
|
||||
return;
|
||||
}
|
||||
const args = func.parameters.map(([, name]) => name).join(", ");
|
||||
|
||||
// We rely on Prettier to clean this up.
|
||||
// We also expect `unimplementedWarning` to already be imported.
|
||||
const methodString = `
|
||||
${methodName}(${args}) {
|
||||
unimplementedWarning("${methodName}");
|
||||
return;
|
||||
}
|
||||
`;
|
||||
|
||||
const lastChild = node.body[node.body.length - 1];
|
||||
context.report({
|
||||
node: node,
|
||||
message: `Missing method ${methodName}`,
|
||||
fix: fixer => {
|
||||
return fixer.insertTextAfter(lastChild, methodString);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
MethodDefinition: function(node) {
|
||||
if (currentObject == null) {
|
||||
return;
|
||||
}
|
||||
const methods = {};
|
||||
currentObject.functions.forEach(func => {
|
||||
methods[func.name.toLowerCase()] = func;
|
||||
});
|
||||
|
||||
const methodName = node.key.name;
|
||||
// Theoretically this should only be implemented on Object, but it's
|
||||
// easier to let each class implement it themselves.
|
||||
if (methodName === "getclassname") {
|
||||
return;
|
||||
}
|
||||
if (methodName === "constructor") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-maki methods may be implemented using the `js_` prefix.
|
||||
if (methodName.startsWith("js_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodName.startsWith("_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const func = methods[methodName];
|
||||
if (func == null) {
|
||||
context.report({
|
||||
node: node.key,
|
||||
message: `Invalid Maki method name \`${methodName}\``,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { params, returnType, body } = node.value;
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
if (returnType == null) {
|
||||
const expectedTypeData = TYPE_MAP[func.result];
|
||||
if (
|
||||
expectedTypeData != null &&
|
||||
!sourceCode.getText(node).includes("unimplementedWarning")
|
||||
) {
|
||||
context.report({
|
||||
node: body,
|
||||
message: `Missing return type for Maki method. Expected \`${expectedTypeData.stringRepresentation}\`.`,
|
||||
fix: fixer => {
|
||||
return fixer.insertTextBefore(
|
||||
body,
|
||||
`: ${expectedTypeData.stringRepresentation}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const expectedTypeData = TYPE_MAP[func.result];
|
||||
if (
|
||||
expectedTypeData != null &&
|
||||
expectedTypeData.typeScriptName !== returnType.typeAnnotation.type
|
||||
) {
|
||||
context.report({
|
||||
node: returnType,
|
||||
message: `Incorrect return type for Maki method. Expected \`${expectedTypeData.stringRepresentation}\`.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
func.parameters.forEach(([type, name], i) => {
|
||||
const expectedTypeData = TYPE_MAP[type.toLowerCase()];
|
||||
|
||||
const actual = params[i];
|
||||
if (actual == null) {
|
||||
context.report({
|
||||
node: node.value,
|
||||
message: `Missing Maki method argument. Expected \`${name}\`.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (actual.name !== name) {
|
||||
// Turned off since some of the maki names are bad.
|
||||
/*
|
||||
context.report({
|
||||
node: node.value.params[i],
|
||||
message: `Invalid Maki method argument name \`${actual}\`. Expected \`${name}\`.`,
|
||||
});
|
||||
*/
|
||||
}
|
||||
if (expectedTypeData == null) {
|
||||
// console.warn(`Missing type data for ${type}.`);
|
||||
return;
|
||||
}
|
||||
const fix = fixer => {
|
||||
return fixer.replaceText(
|
||||
actual,
|
||||
`${actual.name}: ${expectedTypeData.stringRepresentation}`
|
||||
);
|
||||
};
|
||||
if (actual.typeAnnotation == null) {
|
||||
context.report({
|
||||
node: actual,
|
||||
message: `Missing type for Maki argument. Expected \`${expectedTypeData.stringRepresentation}\`.`,
|
||||
fix,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const actualTypeScriptName =
|
||||
actual.typeAnnotation.typeAnnotation.type;
|
||||
|
||||
if (actualTypeScriptName !== expectedTypeData.typeScriptName) {
|
||||
context.report({
|
||||
node: actual,
|
||||
message: `Invalid type for Maki argument. Expected \`${expectedTypeData.typeScriptName}\`.`,
|
||||
fix,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
"maki-class": makiClassRule,
|
||||
"maki-missing-methods": makiMissingMethods,
|
||||
"maki-method-types": makiMethodTypes,
|
||||
};
|
||||
|
|
|
|||
60
modern/eslint/maki-class.js
Normal file
60
modern/eslint/maki-class.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
const {
|
||||
getMakiObjectfromClassDeclarationNode,
|
||||
normalizeClassName,
|
||||
} = require("./maki-eslint-utils");
|
||||
|
||||
function isJsMakiNode(node) {
|
||||
const className = node.id.name;
|
||||
// https://github.com/captbaritone/webamp/pull/828#issuecomment-518023519
|
||||
return className.startsWith("Js");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Ensure Maki Classes match std.mi",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
create: function(context) {
|
||||
return {
|
||||
ClassDeclaration: function(node) {
|
||||
const className = node.id.name;
|
||||
if (isJsMakiNode(node)) {
|
||||
return;
|
||||
}
|
||||
const currentObject = getMakiObjectfromClassDeclarationNode(node);
|
||||
if (currentObject == null) {
|
||||
context.report({
|
||||
node: node.id,
|
||||
message: `Unknown Maki Class \`${className}\`.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const expectedParentClassName = normalizeClassName(
|
||||
currentObject.parent
|
||||
);
|
||||
if (expectedParentClassName == null) {
|
||||
if (node.superClass !== null) {
|
||||
context.report({
|
||||
node: node.id,
|
||||
message: `Unexpected parent class for \`${className}\`. \`${className}\` should not extend any class.`,
|
||||
});
|
||||
}
|
||||
// This is probably MakiObject which does not inherit from anything
|
||||
return;
|
||||
}
|
||||
const parentClassName = node.superClass.name;
|
||||
if (parentClassName !== expectedParentClassName) {
|
||||
context.report({
|
||||
node: node.superClass,
|
||||
message: `Incorrect parent class \`${parentClassName}\`. Expected \`${expectedParentClassName}\`.`,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
26
modern/eslint/maki-eslint-utils.js
Normal file
26
modern/eslint/maki-eslint-utils.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const { objects } = require("../src/maki-interpreter/objects");
|
||||
|
||||
const classNameMappings = {
|
||||
Object: "MakiObject",
|
||||
"@{00000000-0000-0000-0000-000000000000}@": null,
|
||||
};
|
||||
|
||||
function normalizeClassName(className) {
|
||||
const normalized = classNameMappings[className];
|
||||
return normalized === undefined ? className : normalized;
|
||||
}
|
||||
|
||||
const objectsByName = {};
|
||||
for (const value of Object.values(objects)) {
|
||||
objectsByName[normalizeClassName(value.name)] = value;
|
||||
}
|
||||
|
||||
function getMakiObjectfromClassDeclarationNode(node) {
|
||||
const className = node.id.name;
|
||||
return objectsByName[className];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMakiObjectfromClassDeclarationNode,
|
||||
normalizeClassName,
|
||||
};
|
||||
175
modern/eslint/maki-method-types.js
Normal file
175
modern/eslint/maki-method-types.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
const {
|
||||
getMakiObjectfromClassDeclarationNode,
|
||||
} = require("./maki-eslint-utils");
|
||||
|
||||
const TYPE_MAP = {
|
||||
// This might be wrong. Maybe it really is an empty string? Or Null?
|
||||
"": {
|
||||
typeScriptName: "TSVoidKeyword",
|
||||
stringRepresentation: "void",
|
||||
},
|
||||
string: {
|
||||
typeScriptName: "TSStringKeyword",
|
||||
stringRepresentation: "string",
|
||||
},
|
||||
double: {
|
||||
typeScriptName: "TSNumberKeyword",
|
||||
stringRepresentation: "number",
|
||||
},
|
||||
int: {
|
||||
typeScriptName: "TSNumberKeyword",
|
||||
stringRepresentation: "number",
|
||||
},
|
||||
boolean: {
|
||||
typeScriptName: "TSBooleanKeyword",
|
||||
stringRepresentation: "boolean",
|
||||
},
|
||||
float: {
|
||||
typeScriptName: "TSNumberKeyword",
|
||||
stringRepresentation: "number",
|
||||
},
|
||||
any: {
|
||||
typeScriptName: "TSAnyKeyword",
|
||||
stringRepresentation: "any",
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Ensure Maki object methods have the corret type",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
create: function(context) {
|
||||
return {
|
||||
MethodDefinition: function(node) {
|
||||
const currentObject = getMakiObjectfromClassDeclarationNode(
|
||||
node.parent.parent
|
||||
);
|
||||
if (currentObject == null) {
|
||||
return;
|
||||
}
|
||||
const methods = {};
|
||||
currentObject.functions.forEach(func => {
|
||||
methods[func.name.toLowerCase()] = func;
|
||||
});
|
||||
|
||||
const methodName = node.key.name;
|
||||
// Theoretically this should only be implemented on Object, but it's
|
||||
// easier to let each class implement it themselves.
|
||||
if (methodName === "getclassname") {
|
||||
return;
|
||||
}
|
||||
if (methodName === "constructor") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-maki methods may be implemented using the `js_` prefix.
|
||||
if (methodName.startsWith("js_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (methodName.startsWith("_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const func = methods[methodName];
|
||||
if (func == null) {
|
||||
context.report({
|
||||
node: node.key,
|
||||
message: `Invalid Maki method name \`${methodName}\``,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { params, returnType, body } = node.value;
|
||||
const sourceCode = context.getSourceCode();
|
||||
|
||||
if (returnType == null) {
|
||||
const expectedTypeData = TYPE_MAP[func.result];
|
||||
if (
|
||||
expectedTypeData != null &&
|
||||
!sourceCode.getText(node).includes("unimplementedWarning")
|
||||
) {
|
||||
context.report({
|
||||
node: body,
|
||||
message: `Missing return type for Maki method. Expected \`${expectedTypeData.stringRepresentation}\`.`,
|
||||
fix: fixer => {
|
||||
return fixer.insertTextBefore(
|
||||
body,
|
||||
`: ${expectedTypeData.stringRepresentation}`
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const expectedTypeData = TYPE_MAP[func.result];
|
||||
if (
|
||||
expectedTypeData != null &&
|
||||
expectedTypeData.typeScriptName !== returnType.typeAnnotation.type
|
||||
) {
|
||||
context.report({
|
||||
node: returnType,
|
||||
message: `Incorrect return type for Maki method. Expected \`${expectedTypeData.stringRepresentation}\`.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
func.parameters.forEach(([type, name], i) => {
|
||||
const expectedTypeData = TYPE_MAP[type.toLowerCase()];
|
||||
|
||||
const actual = params[i];
|
||||
if (actual == null) {
|
||||
context.report({
|
||||
node: node.value,
|
||||
message: `Missing Maki method argument. Expected \`${name}\`.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (actual.name !== name) {
|
||||
// Turned off since some of the maki names are bad.
|
||||
/*
|
||||
context.report({
|
||||
node: node.value.params[i],
|
||||
message: `Invalid Maki method argument name \`${actual}\`. Expected \`${name}\`.`,
|
||||
});
|
||||
*/
|
||||
}
|
||||
if (expectedTypeData == null) {
|
||||
// console.warn(`Missing type data for ${type}.`);
|
||||
return;
|
||||
}
|
||||
const fix = fixer => {
|
||||
return fixer.replaceText(
|
||||
actual,
|
||||
`${actual.name}: ${expectedTypeData.stringRepresentation}`
|
||||
);
|
||||
};
|
||||
if (actual.typeAnnotation == null) {
|
||||
context.report({
|
||||
node: actual,
|
||||
message: `Missing type for Maki argument. Expected \`${expectedTypeData.stringRepresentation}\`.`,
|
||||
fix,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const actualTypeScriptName =
|
||||
actual.typeAnnotation.typeAnnotation.type;
|
||||
|
||||
if (actualTypeScriptName !== expectedTypeData.typeScriptName) {
|
||||
context.report({
|
||||
node: actual,
|
||||
message: `Invalid type for Maki argument. Expected \`${expectedTypeData.typeScriptName}\`.`,
|
||||
fix,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
59
modern/eslint/maki-missing-methods.js
Normal file
59
modern/eslint/maki-missing-methods.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
const {
|
||||
getMakiObjectfromClassDeclarationNode,
|
||||
} = require("./maki-eslint-utils");
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: "Ensure Maki objects match std.mi",
|
||||
category: "Possible Errors",
|
||||
recommended: false,
|
||||
},
|
||||
schema: [],
|
||||
fixable: "code",
|
||||
},
|
||||
create: function(context) {
|
||||
return {
|
||||
ClassBody: function(node) {
|
||||
const currentObject = getMakiObjectfromClassDeclarationNode(
|
||||
node.parent
|
||||
);
|
||||
if (currentObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const implementedMethodNames = new Set(
|
||||
node.body
|
||||
.filter(prop => prop.type === "MethodDefinition")
|
||||
.map(method => method.key.name)
|
||||
);
|
||||
|
||||
currentObject.functions.forEach(func => {
|
||||
const methodName = func.name.toLowerCase();
|
||||
if (implementedMethodNames.has(methodName)) {
|
||||
return;
|
||||
}
|
||||
const args = func.parameters.map(([, name]) => name).join(", ");
|
||||
|
||||
// We rely on Prettier to clean this up.
|
||||
// We also expect `unimplementedWarning` to already be imported.
|
||||
const methodString = `
|
||||
${methodName}(${args}) {
|
||||
unimplementedWarning("${methodName}");
|
||||
return;
|
||||
}
|
||||
`;
|
||||
|
||||
const lastChild = node.body[node.body.length - 1];
|
||||
context.report({
|
||||
node: node,
|
||||
message: `Missing method ${methodName}`,
|
||||
fix: fixer => {
|
||||
return fixer.insertTextAfter(lastChild, methodString);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
// TODO: Turn these all back on
|
||||
// For now we want to be able to define maki method arguments even though we don't use them.
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"local-rules/maki-methods": "error"
|
||||
"local-rules/maki-missing-methods": "error",
|
||||
"local-rules/maki-class": "error",
|
||||
"local-rules/maki-method-types": "error"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue