mirror of
https://github.com/bendtherules/ask262.git
synced 2026-08-18 21:31:46 +00:00
Merge commit '6e92d2345e8231a44556ce7d416451f5f3e6c398' as 'engine262'
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import mathematicalValue from './mathematical-value.mjs';
|
||||
import safeFunctionWithQ from './safe-function-with-q.mjs';
|
||||
import noFloatingGenerator from './no-floating-generator.mjs';
|
||||
|
||||
export const rules = {
|
||||
'mathematical-value': mathematicalValue,
|
||||
'safe-function-with-q': safeFunctionWithQ,
|
||||
'no-floating-generator': noFloatingGenerator,
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import path from 'node:path';
|
||||
import type { Rule, Scope } from 'eslint';
|
||||
import type * as ESTree from 'estree';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
fixable: 'code',
|
||||
hasSuggestions: true,
|
||||
},
|
||||
create(context) {
|
||||
type FixableCallExpression = ESTree.CallExpression & { callee: ESTree.MemberExpression & { computed: false, property: ESTree.Identifier } };
|
||||
|
||||
let needsImportForR: { node: FixableCallExpression, fixable: boolean, reachable: boolean, name: string }[];
|
||||
let importSpecifiersForR: ESTree.ImportSpecifier[];
|
||||
let importForAllModule: ESTree.ImportDeclaration | undefined;
|
||||
let importForSpecTypesModule: ESTree.ImportDeclaration | undefined;
|
||||
let lastImport: ESTree.ImportDeclaration | undefined;
|
||||
const pathToAbstractOps = `${path.resolve(context.cwd, 'src/abstract-ops').replaceAll('\\', '/')}/`;
|
||||
const pathToSpecTypesModule = path.resolve(pathToAbstractOps, 'spec-types.mjs').replaceAll('\\', '/');
|
||||
const pathToAllModule = path.resolve(pathToAbstractOps, 'all.mjs').replaceAll('\\', '/');
|
||||
|
||||
return {
|
||||
Program() {
|
||||
needsImportForR = [];
|
||||
importSpecifiersForR = [];
|
||||
importForAllModule = undefined;
|
||||
importForSpecTypesModule = undefined;
|
||||
lastImport = undefined;
|
||||
},
|
||||
'Program:exit': function Program_exit(node) {
|
||||
for (const importName of ['R', 'MathematicalValue']) {
|
||||
const needsImportForRNonFixable = needsImportForR.filter((entry) => !entry.fixable && entry.reachable && entry.name === importName);
|
||||
if (needsImportForRNonFixable.length) {
|
||||
// If some calls aren't fixable and there are no imports of 'R', report the need to import 'R'.
|
||||
// Include a fix, if possible.
|
||||
const importNamePart = importName === 'R' ? 'R' : `R (imported as ${importName})`;
|
||||
const importSpecifier = importName === 'R' ? 'R' : `R as ${importName}`;
|
||||
const fixable = !lookup(context.sourceCode.getScope(node), importName);
|
||||
const fix: Rule.ReportFixer = function* fix(fixer) {
|
||||
if (importForSpecTypesModule) {
|
||||
const last = importForSpecTypesModule.specifiers.at(-1)!;
|
||||
yield fixer.insertTextAfter(last, `, ${importSpecifier}`);
|
||||
} else if (importForAllModule) {
|
||||
const last = importForAllModule.specifiers.at(-1)!;
|
||||
yield fixer.insertTextAfter(last, `, ${importSpecifier}`);
|
||||
} else {
|
||||
const filename = path.resolve(context.filename).replaceAll('\\', '/');
|
||||
let relativePath;
|
||||
if (filename.startsWith(pathToAbstractOps)) {
|
||||
relativePath = path.relative(path.dirname(filename), pathToSpecTypesModule).replaceAll('\\', '/');
|
||||
} else {
|
||||
relativePath = path.relative(path.dirname(filename), pathToAllModule).replaceAll('\\', '/');
|
||||
}
|
||||
if (!path.isAbsolute(relativePath)
|
||||
&& !relativePath.startsWith('../')
|
||||
&& !relativePath.startsWith('./')) {
|
||||
relativePath = `./${relativePath}`;
|
||||
}
|
||||
if (lastImport) {
|
||||
yield fixer.insertTextAfter(lastImport, `\nimport { ${importSpecifier} } from ${JSON.stringify(relativePath)};`);
|
||||
} else {
|
||||
yield fixer.insertTextAfterRange([0, 0], `import { ${importSpecifier} } from ${JSON.stringify(relativePath)};\n`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
context.report({
|
||||
node: needsImportForRNonFixable[0].node,
|
||||
message: `Import ${importNamePart} to convert mathematical values`,
|
||||
fix: fixable ? fix : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const { node: callNode, fixable, name } of needsImportForR) {
|
||||
const fix: Rule.ReportFixer = function* fix(fixer) {
|
||||
// foo.numberValue()
|
||||
// -> foo
|
||||
yield fixer.removeRange([callNode.callee.object.range![1], callNode.range![1]]);
|
||||
|
||||
// foo
|
||||
// -> R(foo)
|
||||
yield fixer.insertTextBefore(callNode, `${name}(`);
|
||||
yield fixer.insertTextAfter(callNode, ')');
|
||||
};
|
||||
|
||||
// Report the need to use 'R'. Include a fix, if possible.
|
||||
const namePart = name === 'R' ? 'R' : `R (imported as ${name})`;
|
||||
const methodNamePart = callNode.callee.property.name;
|
||||
context.report({
|
||||
node: callNode.callee,
|
||||
message: `Use ${namePart}, not .${methodNamePart}(), to get a mathematical value`,
|
||||
fix: fixable ? fix : undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
ImportDeclaration(node) {
|
||||
lastImport = node;
|
||||
switch (isImportOfRModule(node)) {
|
||||
case 'spec-types':
|
||||
importForSpecTypesModule ??= node;
|
||||
break;
|
||||
case 'all':
|
||||
importForAllModule ??= node;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
ImportSpecifier(node) {
|
||||
if (isImportOfR(node)) {
|
||||
importSpecifiersForR.push(node);
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (node.callee.type === 'MemberExpression'
|
||||
&& node.callee.computed === false
|
||||
&& node.callee.property.type === 'Identifier') {
|
||||
if (node.callee.property.name === 'numberValue'
|
||||
|| node.callee.property.name === 'bigintValue') {
|
||||
const { fixable, reachable, name } = getUsableReferenceToR(context.sourceCode.getScope(node));
|
||||
needsImportForR.push({
|
||||
node: node as FixableCallExpression, fixable, reachable, name,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function lookup(scope: Scope.Scope | null, name: string) {
|
||||
while (scope) {
|
||||
const v = scope.set.get(name);
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
scope = scope.upper;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isImportOfRModule(node: ESTree.ImportDeclaration) {
|
||||
if (!node.specifiers.length) {
|
||||
// `import {} from ...` not currently usable
|
||||
}
|
||||
if (node.specifiers.length && node.specifiers[0].type === 'ImportNamespaceSpecifier') {
|
||||
// `import * as ns from ...` not currently usable
|
||||
return false;
|
||||
}
|
||||
if (node.specifiers.length && node.specifiers[0].type === 'ImportDefaultSpecifier') {
|
||||
// `import X from ...` and `import X, {} from ...` not currently usable
|
||||
return false;
|
||||
}
|
||||
const importPath = path.resolve(path.dirname(context.filename), node.source.value as string).replaceAll('\\', '/');
|
||||
if (importPath === pathToAllModule) {
|
||||
return 'all';
|
||||
} else if (importPath === pathToSpecTypesModule) {
|
||||
return 'spec-types';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isImportOfR(node: ESTree.ImportSpecifier & Rule.NodeParentExtension) {
|
||||
if ((node.imported as ESTree.Identifier).name === 'R'
|
||||
&& node.parent.type === 'ImportDeclaration') {
|
||||
return !!isImportOfRModule(node.parent);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUsableReferenceToR(scope: Scope.Scope) {
|
||||
let candidate;
|
||||
for (const spec of importSpecifiersForR) {
|
||||
const varDecl = lookup(scope, spec.local.name);
|
||||
if (varDecl?.defs.some((def) => def.type === 'ImportBinding' && def.node === spec)) {
|
||||
if (spec.local.name === 'R') {
|
||||
// prefer 'R' if it is found
|
||||
return { fixable: true, reachable: true, name: spec.local.name };
|
||||
}
|
||||
if (spec.local.name === 'MathematicalValue') {
|
||||
candidate = 'MathematicalValue';
|
||||
} else {
|
||||
candidate ??= spec.local.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we found a candidate, return it
|
||||
if (candidate) {
|
||||
return { fixable: true, reachable: true, name: candidate };
|
||||
}
|
||||
|
||||
// if no imports were found, try R
|
||||
if (!lookup(scope, 'R')) {
|
||||
return { fixable: false, reachable: true, name: 'R' };
|
||||
}
|
||||
|
||||
// if R isn't reachable, try MathematicalValue
|
||||
if (!lookup(scope, 'MathematicalValue')) {
|
||||
return { fixable: false, reachable: true, name: 'MathematicalValue' };
|
||||
}
|
||||
|
||||
// no imports were found or usable
|
||||
return { fixable: false, reachable: false, name: 'R' };
|
||||
}
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Rule } from 'eslint';
|
||||
import type { ParserServices } from '@typescript-eslint/parser';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import ts from 'typescript';
|
||||
|
||||
declare module 'typescript' {
|
||||
interface Type {
|
||||
typeArguments?: ts.Type[];
|
||||
}
|
||||
}
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
messages: {
|
||||
floating: 'Generator is not stepped. It should be yield* evaluator',
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
const services = getParserServices(context);
|
||||
if (!services.program) {
|
||||
throw new Error('No ts program found');
|
||||
}
|
||||
const checker = services.program.getTypeChecker();
|
||||
const GeneratorSymbol = checker.resolveName('Generator', undefined, ts.SymbolFlags.Interface, false);
|
||||
const AsyncGeneratorSymbol = checker.resolveName('AsyncGenerator', undefined, ts.SymbolFlags.Interface, false);
|
||||
if (!GeneratorSymbol || !AsyncGeneratorSymbol) {
|
||||
throw new Error('Cannot find necessary symbols');
|
||||
}
|
||||
|
||||
return {
|
||||
'ExpressionStatement[expression.type="CallExpression"]':
|
||||
(function VisitCallExpression({ expression }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const callNode = services.esTreeNodeToTSNodeMap.get(expression as any);
|
||||
if (!ts.isCallExpression(callNode)) {
|
||||
return;
|
||||
}
|
||||
const callType = checker.getTypeAtLocation(callNode);
|
||||
if (callType?.getSymbol() === GeneratorSymbol) {
|
||||
context.report({
|
||||
node: expression,
|
||||
messageId: 'floating',
|
||||
* fix(fixer) {
|
||||
yield fixer.insertTextBefore(expression, 'yield* ');
|
||||
},
|
||||
});
|
||||
}
|
||||
} satisfies Rule.RuleListener['ExpressionStatement']),
|
||||
};
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
|
||||
export default rule;
|
||||
|
||||
function getParserServices(context: Rule.RuleContext): ParserServices {
|
||||
if (
|
||||
context.sourceCode.parserServices?.esTreeNodeToTSNodeMap == null
|
||||
|| context.sourceCode.parserServices.tsNodeToESTreeNodeMap == null
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (context.sourceCode.parserServices.program == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return context.sourceCode.parserServices;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@engine262/eslint-plugin",
|
||||
"version": "0.0.0",
|
||||
"main": "./lib/index.mjs",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { resolve } from 'node:path';
|
||||
import type { Rule } from 'eslint';
|
||||
import type { ParserServices } from '@typescript-eslint/parser';
|
||||
import type { TSESTree } from '@typescript-eslint/types';
|
||||
import type * as ESTree from 'estree';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import ts from 'typescript';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
declare module 'typescript' {
|
||||
interface Type {
|
||||
typeArguments?: ts.Type[];
|
||||
}
|
||||
}
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
messages: {
|
||||
noAbruptCompletion: 'The function return type does not include AbruptCompletion',
|
||||
noThrowCompletion: 'The function return type does not include ThrowCompletion',
|
||||
noNeedToUseQ: 'Unnecessary Q() call',
|
||||
evaluator: 'It should be Q(yield* evaluator) instead of Q(evaluator)',
|
||||
},
|
||||
fixable: 'code',
|
||||
},
|
||||
create(context) {
|
||||
const services = getParserServices(context);
|
||||
if (!services.program) {
|
||||
throw new Error('No ts program found');
|
||||
}
|
||||
const checker = services.program.getTypeChecker();
|
||||
const CompletionFile = services.program.getSourceFile(resolve(__dirname, '../../../src/completion.mts'));
|
||||
const PromiseFile = services.program.getSourceFile(resolve(__dirname, '../../../src/intrinsics/Promise.mts'));
|
||||
if (!CompletionFile || !PromiseFile) {
|
||||
throw new Error('Cannot load src/completion.mts or src/intrinsics/Promise.mts');
|
||||
}
|
||||
const AbruptCompletion = CompletionFile.statements.find((s) => ts.isTypeAliasDeclaration(s) && s.name.text === 'AbruptCompletion');
|
||||
const ThrowCompletion = CompletionFile.statements.find((s) => ts.isTypeAliasDeclaration(s) && s.name.text === 'ThrowCompletion');
|
||||
const PromiseObject = PromiseFile.statements.find((s) => ts.isInterfaceDeclaration(s) && s.name.text === 'PromiseObject');
|
||||
const GeneratorSymbol = checker.resolveName('Generator', undefined, ts.SymbolFlags.Interface, false);
|
||||
if (!AbruptCompletion || !PromiseObject || !ThrowCompletion || !GeneratorSymbol) {
|
||||
throw new Error('Cannot find necessary symbols');
|
||||
}
|
||||
const AbruptCompletionType = checker.getTypeAtLocation(AbruptCompletion);
|
||||
const ThrowCompletionType = checker.getTypeAtLocation(ThrowCompletion);
|
||||
const PromiseObjectType = checker.getTypeAtLocation(PromiseObject);
|
||||
const reported = new WeakSet();
|
||||
|
||||
return {
|
||||
// eslint-disable-next-line func-names
|
||||
"CallExpression[callee.name='Q'],[callee.name='ReturnIfAbrupt'],[callee.name='IfAbruptRejectPromise'],[callee.name='IfAbruptCloseIterator']":
|
||||
(function (node) { // eslint-disable-line func-names
|
||||
const firstArg = node.arguments[0];
|
||||
if (firstArg?.type === 'SpreadElement') {
|
||||
return;
|
||||
}
|
||||
|
||||
const containingFunction = ts.findAncestor(services.esTreeNodeToTSNodeMap.get(node as TSESTree.Node), ts.isFunctionLike);
|
||||
if (!containingFunction) {
|
||||
throw new Error('Cannot find containing function');
|
||||
}
|
||||
|
||||
const firstArgType = checker.getTypeAtLocation(services.esTreeNodeToTSNodeMap.get(firstArg as TSESTree.Node));
|
||||
if (firstArgType?.getSymbol() === GeneratorSymbol) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'evaluator',
|
||||
* fix(fixer) {
|
||||
yield fixer.insertTextBefore(firstArg, 'yield* ');
|
||||
},
|
||||
});
|
||||
}
|
||||
const containingFunctionType = checker.getTypeAtLocation(containingFunction);
|
||||
if ((containingFunctionType.flags & ts.TypeFlags.Any) || (containingFunctionType.flags & ts.TypeFlags.Any)) {
|
||||
throw new Error('Unexpected any');
|
||||
}
|
||||
|
||||
let returnType = containingFunctionType.getCallSignatures().at(-1)?.getReturnType();
|
||||
if (ts.isMethodDeclaration(containingFunction) && ts.isIdentifier(containingFunction.name) && ts.isExpression(containingFunction.parent)) {
|
||||
const contextualObjectType = checker.getContextualType(containingFunction.parent);
|
||||
const currentFunctionName = containingFunction.name;
|
||||
if (contextualObjectType) {
|
||||
const contextualPropertySymbol = contextualObjectType.getProperty(currentFunctionName.text);
|
||||
if (contextualPropertySymbol) {
|
||||
let contextualPropertyType = checker.getTypeOfSymbol(contextualPropertySymbol);
|
||||
if (contextualPropertyType.isUnion()) {
|
||||
const excludeUndefined = contextualPropertyType.types.find((x) => x.flags & ~ts.TypeFlags.Undefined);
|
||||
if (excludeUndefined) {
|
||||
contextualPropertyType = excludeUndefined;
|
||||
}
|
||||
}
|
||||
returnType = contextualPropertyType.getCallSignatures().at(-1)?.getReturnType();
|
||||
// returnType && console.log('returnType', checker.typeToString(returnType));
|
||||
}
|
||||
}
|
||||
}
|
||||
// internal api. no api to insatiate the global Generator type.
|
||||
if (returnType?.getSymbol() === GeneratorSymbol && returnType?.typeArguments?.[1]) {
|
||||
returnType = returnType.typeArguments[1];
|
||||
}
|
||||
if (!returnType) {
|
||||
throw new Error('Cannot find return type');
|
||||
}
|
||||
|
||||
const f = (node.callee as ESTree.Identifier).name;
|
||||
let ExpectedReturnType;
|
||||
// const ExpectedReturnType = f === 'IfAbruptRejectPromise' ? PromiseObjectType : AbruptCompletionType;
|
||||
if (checker.isTypeAssignableTo(AbruptCompletionType, firstArgType)) {
|
||||
ExpectedReturnType = AbruptCompletionType;
|
||||
}
|
||||
if (checker.isTypeAssignableTo(ThrowCompletionType, firstArgType)) {
|
||||
ExpectedReturnType = ThrowCompletionType;
|
||||
}
|
||||
if (f === 'IfAbruptRejectPromise') {
|
||||
ExpectedReturnType = PromiseObjectType;
|
||||
}
|
||||
if (!ExpectedReturnType) {
|
||||
// context.report({
|
||||
// node,
|
||||
// messageId: 'noNeedToUseQ',
|
||||
// });
|
||||
return;
|
||||
}
|
||||
if (reported.has(containingFunction)) {
|
||||
return;
|
||||
}
|
||||
reported.add(containingFunction);
|
||||
if (!checker.isTypeAssignableTo(ExpectedReturnType, returnType)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: ExpectedReturnType === AbruptCompletionType ? 'noAbruptCompletion' : 'noThrowCompletion',
|
||||
});
|
||||
}
|
||||
} satisfies Rule.RuleListener['CallExpression']),
|
||||
};
|
||||
},
|
||||
} satisfies Rule.RuleModule;
|
||||
|
||||
export default rule;
|
||||
|
||||
function getParserServices(context: Rule.RuleContext): ParserServices {
|
||||
if (
|
||||
context.sourceCode.parserServices?.esTreeNodeToTSNodeMap == null
|
||||
|| context.sourceCode.parserServices.tsNodeToESTreeNodeMap == null
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (context.sourceCode.parserServices.program == null) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return context.sourceCode.parserServices;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"declarationMap": false,
|
||||
"erasableSyntaxOnly": true,
|
||||
"allowImportingTsExtensions": false,
|
||||
"strict": false
|
||||
},
|
||||
"include": ["./"]
|
||||
}
|
||||
Reference in New Issue
Block a user