Use .at() and Object.hasOwn() (#13482)

This commit is contained in:
fisker Cheung authored and GitHub committed 2022-09-18 17:50:14 +08:00
1 parent 8cfdcaa6b2
commit 9bacc75772
51 files changed
+254 -263

No files matched your search

+6
View File
@@ -14,6 +14,7 @@ import esbuildPluginStripNodeProtocol from "./esbuild-plugins/strip-node-protoco
import esbuildPluginThrowWarnings from "./esbuild-plugins/throw-warnings.mjs";
import esbuildPluginShimCommonjsObjects from "./esbuild-plugins/shim-commonjs-objects.mjs";
import bundles from "./config.mjs";
import transform from "./transform/index.js";
const { dirname, readJsonSync, require } = createEsmUtils(import.meta);
const packageJson = readJsonSync("../../package.json");
@@ -42,6 +43,11 @@ function* getEsbuildOptions(bundle, buildOptions) {
find: "const __dirname = path.dirname(fileURLToPath(import.meta.url));",
replacement: "",
},
// Transform `.at` and `Object.hasOwn`
{
module: "*",
process: transform,
},
// #12493, not sure what the problem is, but replace the cjs version with esm version seems fix it
{
module: require.resolve("tslib"),
+1 -1
View File
@@ -10,7 +10,7 @@ export default function esbuildPluginEvaluate() {
build.onLoad({ filter: /\.evaluate\.js$/ }, async ({ path }) => {
let data = await importModule(path);
if (Object.prototype.hasOwnProperty.call(data, "default")) {
if (Object.hasOwn(data, "default")) {
if (Object.keys(data).length !== 1) {
throw new TypeError("Mixed export not allowed.");
}
@@ -16,9 +16,9 @@ export default function esbuildPluginInteropDefault() {
const text = fs.readFileSync(file, "utf8").trim();
const lines = text.split("\n");
if (
lines[lines.length - 2] !==
lines.at(-2) !==
"// Annotate the CommonJS export names for ESM import in node:" ||
lines[lines.length - 1] !== "0 && (module.exports = {});"
lines.at(-1) !== "0 && (module.exports = {});"
) {
throw new Error("Unexpected output.");
}
@@ -174,7 +174,7 @@ function setupOnLoadListener(build, { concepts, replacements, processors }) {
let text = original;
for (const process of processFunctions) {
text = process(text);
text = process(text, file);
}
// For files not JavaScript, we need add correct `loader` to the result,
+13
View File
@@ -0,0 +1,13 @@
/*
Note:
1. `acorn` contains `RegExpValidationState` class with `.at` method (2 arguments)
2. `postcss` contains `Container` class with `.at` method
*/
const at = (object, index, ...args) =>
object.at
? object.at(index, ...args)
: object[index < 0 ? object.length + index : index];
export default at;
+113
View File
@@ -0,0 +1,113 @@
import { fileURLToPath } from "node:url";
import { parse } from "@babel/parser";
import { traverseFast as traverse } from "@babel/types";
import babelGenerator from "@babel/generator";
import { outdent } from "outdent";
import { SOURCE_DIR } from "../../utils/index.mjs";
const generate = babelGenerator.default;
const atHelperPath = fileURLToPath(new URL("../shims/at.js", import.meta.url));
/* Doesn't work for dependencies, optional chaining, and spread arguments */
// `Object.hasOwn(foo, "bar")` -> `Object.prototype.hasOwnProperty.call(foo, "bar")`
function transformObjectHasOwnCall(node) {
if (
!(
node.type === "CallExpression" &&
!node.optional &&
node.arguments.length === 2 &&
node.arguments.every(({ type }) => type !== "SpreadElement") &&
node.callee.type === "MemberExpression" &&
!node.callee.optional &&
!node.callee.computed &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "Object" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "hasOwn"
)
) {
return false;
}
node.callee = {
type: "MemberExpression",
object: {
type: "MemberExpression",
object: {
type: "MemberExpression",
object: { type: "Identifier", name: "Object" },
property: { type: "Identifier", name: "prototype" },
},
property: { type: "Identifier", name: "hasOwnProperty" },
},
property: { type: "Identifier", name: "call" },
};
return true;
}
// `foo.at(index)` -> `__at(foo, index)`
function transformRelativeIndexingCall(node) {
if (
!(
node.type === "CallExpression" &&
!node.optional &&
node.arguments.length === 1 &&
node.arguments.every(({ type }) => type !== "SpreadElement") &&
node.callee.type === "MemberExpression" &&
!node.callee.optional &&
!node.callee.computed &&
node.callee.object.type !== "ThisExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "at"
)
) {
return false;
}
node.arguments.unshift(node.callee.object);
node.callee = { type: "Identifier", name: "__at" };
return true;
}
function transform(original, file) {
if (
file === atHelperPath ||
!file.startsWith(SOURCE_DIR) ||
(!original.includes(".at(") && !original.includes("Object.hasOwn("))
) {
return original;
}
let changed = false;
let shouldInjectRelativeIndexingHelper = false;
const ast = parse(original, { sourceType: "module" });
traverse(ast, (node) => {
const hasObjectHasOwnCall = transformObjectHasOwnCall(node);
changed ||= hasObjectHasOwnCall;
const hasRelativeIndexingCall = transformRelativeIndexingCall(node);
shouldInjectRelativeIndexingHelper ||= hasRelativeIndexingCall;
changed ||= hasRelativeIndexingCall;
});
if (!changed) {
return original;
}
let { code } = generate(ast);
if (shouldInjectRelativeIndexingHelper) {
code = outdent`
import __at from ${JSON.stringify(atHelperPath)};
${code}
`;
}
return code;
}
export default transform;
@@ -1,71 +0,0 @@
"use strict";
const selector = [
"MemberExpression",
"[computed=true]",
"[optional=false]",
'[property.type="BinaryExpression"]',
'[property.operator="-"]',
'[property.left.type="MemberExpression"]',
"[property.left.optional=false]",
"[property.left.computed=false]",
'[property.left.property.type="Identifier"]',
'[property.left.property.name="length"]',
'[property.right.type="Literal"]',
`:not(${[
"AssignmentExpression > .left",
"UpdateExpression > .argument",
// Ignore `getPenultimate` and `getLast` function self
'VariableDeclarator[id.name="getPenultimate"] > ArrowFunctionExpression.init *',
'VariableDeclarator[id.name="getLast"] > ArrowFunctionExpression.init *',
].join(", ")})`,
].join("");
const messageId = "consistent-negative-index-access";
module.exports = {
meta: {
type: "suggestion",
docs: {
url: "https://github.com/prettier/prettier/blob/main/scripts/tools/eslint-plugin-prettier-internal-rules/consistent-negative-index-access.js",
},
messages: {
[messageId]: "Prefer `{{method}}(…)` over `…[….length - {{index}}]`.",
},
fixable: "code",
},
create(context) {
const sourceCode = context.getSourceCode();
return {
[selector](node) {
const { value: index } = node.property.right;
if (index !== 1 && index !== 2) {
return;
}
const { object } = node;
const lengthObject = node.property.left.object;
const objectText = sourceCode.getText(object);
// Simply use text to compare object
if (sourceCode.getText(lengthObject) !== objectText) {
return;
}
const method = ["getLast", "getPenultimate"][index - 1];
context.report({
node,
messageId,
data: {
index,
method,
},
fix: (fixer) => fixer.replaceText(node, `${method}(${objectText})`),
});
},
};
},
};
@@ -4,7 +4,6 @@ module.exports = {
rules: {
"await-cli-tests": require("./await-cli-tests.js"),
"better-parent-property-check-in-needs-parens": require("./better-parent-property-check-in-needs-parens.js"),
"consistent-negative-index-access": require("./consistent-negative-index-access.js"),
"directly-loc-start-end": require("./directly-loc-start-end.js"),
"flat-ast-path-call": require("./flat-ast-path-call.js"),
"jsx-identifier-case": require("./jsx-identifier-case.js"),
@@ -79,50 +79,6 @@ test("better-parent-property-check-in-needs-parens", {
})),
});
test("consistent-negative-index-access", {
valid: [
"getLast(foo)",
"getPenultimate(foo)",
"foo[foo.length]",
"foo[foo.length - 3]",
"foo[foo.length + 1]",
"foo[foo.length + -1]",
"foo[foo.length * -1]",
"foo.length - 1",
"foo?.[foo.length - 1]",
"foo[foo?.length - 1]",
"foo[foo['length'] - 1]",
"foo[bar.length - 1]",
"foo.bar[foo. bar.length - 1]",
"foo[foo.length - 1]++",
"--foo[foo.length - 1]",
"foo[foo.length - 1] += 1",
"foo[foo.length - 1] = 1",
],
invalid: [
{
code: "foo[foo.length - 1]",
output: "getLast(foo)",
errors: 1,
},
{
code: "foo[foo.length - 2]",
output: "getPenultimate(foo)",
errors: 1,
},
{
code: "foo[foo.length - 0b10]",
output: "getPenultimate(foo)",
errors: 1,
},
{
code: "foo()[foo().length - 1]",
output: "getLast(foo())",
errors: 1,
},
],
});
test("directly-loc-start-end", {
valid: [],
invalid: [
+2
View File
@@ -32,11 +32,13 @@ async function writeFile(file, content) {
const PROJECT_ROOT = url.fileURLToPath(new URL("../../", import.meta.url));
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
const WEBSITE_DIR = path.join(PROJECT_ROOT, "website");
const SOURCE_DIR = path.join(PROJECT_ROOT, "src");
export {
PROJECT_ROOT,
DIST_DIR,
WEBSITE_DIR,
SOURCE_DIR,
readJson,
writeJson,
writeFile,