From a3d6615f3ff70efd1e572c17ea05e75cb572660c Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sun, 22 Dec 2019 15:53:30 -0800 Subject: [PATCH] Split up Maki eslint rules --- eslint-local-rules.js | 266 +------------------------- modern/eslint/maki-class.js | 60 ++++++ modern/eslint/maki-eslint-utils.js | 26 +++ modern/eslint/maki-method-types.js | 175 +++++++++++++++++ modern/eslint/maki-missing-methods.js | 59 ++++++ modern/src/runtime/.eslintrc | 4 +- 6 files changed, 329 insertions(+), 261 deletions(-) create mode 100644 modern/eslint/maki-class.js create mode 100644 modern/eslint/maki-eslint-utils.js create mode 100644 modern/eslint/maki-method-types.js create mode 100644 modern/eslint/maki-missing-methods.js diff --git a/eslint-local-rules.js b/eslint-local-rules.js index d70dc24a..f773912a 100644 --- a/eslint-local-rules.js +++ b/eslint-local-rules.js @@ -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, }; diff --git a/modern/eslint/maki-class.js b/modern/eslint/maki-class.js new file mode 100644 index 00000000..198f8bd3 --- /dev/null +++ b/modern/eslint/maki-class.js @@ -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}\`.`, + }); + } + }, + }; + }, +}; diff --git a/modern/eslint/maki-eslint-utils.js b/modern/eslint/maki-eslint-utils.js new file mode 100644 index 00000000..48634bc3 --- /dev/null +++ b/modern/eslint/maki-eslint-utils.js @@ -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, +}; diff --git a/modern/eslint/maki-method-types.js b/modern/eslint/maki-method-types.js new file mode 100644 index 00000000..5be78e60 --- /dev/null +++ b/modern/eslint/maki-method-types.js @@ -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, + }); + } + }); + }, + }; + }, +}; diff --git a/modern/eslint/maki-missing-methods.js b/modern/eslint/maki-missing-methods.js new file mode 100644 index 00000000..0e65db01 --- /dev/null +++ b/modern/eslint/maki-missing-methods.js @@ -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); + }, + }); + }); + }, + }; + }, +}; diff --git a/modern/src/runtime/.eslintrc b/modern/src/runtime/.eslintrc index bdfd9f78..8cdc3f29 100644 --- a/modern/src/runtime/.eslintrc +++ b/modern/src/runtime/.eslintrc @@ -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" } }