Skip to content

Commit

Permalink
feat: support complex identifiers
Browse files Browse the repository at this point in the history
addresses #64
  • Loading branch information
Sec-ant committed Jan 17, 2024
1 parent 098e82b commit 68567da
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 37 deletions.
17 changes: 4 additions & 13 deletions src/printers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TemplateLiteral } from "estree";
import type { Node, TemplateLiteral } from "estree";
import type { AstPath, Options, Plugin, Printer } from "prettier";
import { builders } from "prettier/doc";
import { printers as estreePrinters } from "prettier/plugins/estree.mjs";
Expand All @@ -7,7 +7,6 @@ import {
embeddedLanguages,
makeIdentifiersOptionName,
} from "./embedded/index.js";
import type { PrettierNode } from "./types.js";
import {
getIdentifierFromComment,
getIdentifierFromTag,
Expand All @@ -21,7 +20,7 @@ const { estree: estreePrinter } = estreePrinters;
// we override the built-in one with this
// so that we can add hooks to support other languages
const embed: Printer["embed"] = function (
path: AstPath<PrettierNode>,
path: AstPath<Node>,
options: Options,
) {
const { node } = path;
Expand All @@ -38,16 +37,8 @@ const embed: Printer["embed"] = function (
continue;
}
const identifier =
getIdentifierFromComment(
path,
identifiers,
options.noEmbeddedIdentificationByComment ?? [],
) ??
getIdentifierFromTag(
path,
identifiers,
options.noEmbeddedIdentificationByTag ?? [],
);
getIdentifierFromComment(path, identifiers, options) ??
getIdentifierFromTag(path, identifiers, options);
if (identifier === undefined) {
continue;
}
Expand Down
25 changes: 20 additions & 5 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Comment, Node as EsTreeNode, TemplateLiteral } from "estree";
import type { TemplateLiteral } from "estree";
import type { AstPath, Doc, Options } from "prettier";
import {
makeIdentifiersOptionName,
Expand All @@ -9,13 +9,28 @@ import {
} from "./embedded/index.js";
import type { PrettierPluginGlobalOptions } from "./options.js";

export type PrettierNode = EsTreeNode & {
comments?: (Comment & {
// patch estree types
declare module "estree" {
interface Comment {
leading: boolean;
trailing: boolean;
nodeDescription: string;
})[];
};
}

interface BaseNode {
comments?: Comment[];
}

interface File extends BaseNode {
type: "File";
sourceType: "script" | "module";
program: Program;
}

interface NodeMap {
File: File;
}
}

export type InternalPrintFun = (
selector?: string | number | (string | number)[] | AstPath<TemplateLiteral>,
Expand Down
179 changes: 160 additions & 19 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import type { Node } from "estree";
import memoize from "micro-memoize";
import { readFile } from "node:fs/promises";
import { dirname, extname, isAbsolute, resolve } from "node:path";
import { Worker } from "node:worker_threads";
import { resolveConfigFile, type AstPath } from "prettier";
import type { EmbeddedOverrides, PrettierNode } from "./types.js";
import {
resolveConfigFile,
type AstPath,
type Options,
type Parser,
type ParserOptions,
} from "prettier";
import { parsers } from "./parsers.js";
import type { EmbeddedOverrides } from "./types.js";

async function importJson(absolutePath: string) {
try {
Expand Down Expand Up @@ -160,14 +168,13 @@ export const resolveEmbeddedOverrideOptions = async (
return undefined;
};

// TODO: support tags like 'this.html', 'this["html"]'..., if possible
// ideally, the best api to use is https://github.com/estools/esquery
// TODO: use esquery for further customization?

// function to get identifier from template literal comments
export function getIdentifierFromComment(
{ node, parent }: AstPath<PrettierNode>,
{ node, parent }: AstPath<Node>,
comments: string[],
noIdentificationList: string[],
options: Options,
): string | undefined {
if (comments.length === 0) {
return;
Expand All @@ -190,11 +197,12 @@ export function getIdentifierFromComment(
) {
return;
}
const noIdentificationList = options.noEmbeddedIdentificationByComment ?? [];
for (const comment of comments) {
if (
` ${comment} ` === lastNodeComment.value &&
!noIdentificationList.includes(comment)
) {
if (noIdentificationList.includes(comment)) {
continue;
}
if (` ${comment} ` === lastNodeComment.value) {
return comment;
}
}
Expand All @@ -203,24 +211,157 @@ export function getIdentifierFromComment(

// function to get identifier from template literal tags
export function getIdentifierFromTag(
{ node, parent }: AstPath<PrettierNode>,
{ node, parent }: AstPath<Node>,
tags: string[],
noIdentificationList: string[],
options: Options,
): string | undefined {
if (tags.length === 0) {
return;
}
if (
tags.length === 0 ||
node.type !== "TemplateLiteral" ||
parent?.type !== "TaggedTemplateExpression" ||
parent.tag.type !== "Identifier"
parent?.type !== "TaggedTemplateExpression"
) {
return;
}
assumeAs<keyof typeof parsers>(options.parser);
const isKnownParser = options.parser in parsers;
const noIdentificationList = options.noEmbeddedIdentificationByTag ?? [];
const ignoreSet = new Set([
"start",
"end",
"loc",
"range",
"filename",
"typeAnnotation",
"decorators",
]);
for (const tag of tags) {
if (parent.tag.name === tag && !noIdentificationList.includes(tag)) {
return tag;
if (noIdentificationList.includes(tag)) {
continue;
}
// simple "identifiers"
if (isLegitJsIdentifier(tag)) {
if (parent.tag.type === "Identifier" && parent.tag.name === tag) {
return tag;
}
}
// complex "identifiers"
else {
if (!isKnownParser) {
return;
}
let referenceTopLevelNode: Node;
try {
const nodeOrPromise = (parsers[options.parser] as Parser<Node>).parse(
`${tag}\`\``,
options as ParserOptions<Node>,
) as Node | Promise<Node>;
if (nodeOrPromise instanceof Promise) {
throw new TypeError(
"Async parse function hasn't been supported yet.",
);
}
referenceTopLevelNode = nodeOrPromise;
} catch {
continue;
}
// babel family parsers have a File type parent node
if (referenceTopLevelNode.type === "File") {
referenceTopLevelNode = referenceTopLevelNode.program;
}
if (
!(
referenceTopLevelNode.type === "Program" &&
referenceTopLevelNode.body[0]?.type === "ExpressionStatement"
)
) {
continue;
}
const referenceNode = referenceTopLevelNode.body[0]?.expression;
if (referenceNode?.type !== "TaggedTemplateExpression") {
continue;
}
assumeAs<Record<string, unknown>>(parent.tag);
assumeAs<Record<string, unknown>>(referenceNode.tag);
if (compareObjects(parent.tag, referenceNode.tag, ignoreSet)) {
return tag;
}
}
}
return;
}

// this function will be stripped at runtime
function assumeAs<T>(_: unknown): asserts _ is T {
/* void */
}

function isLegitJsIdentifier(identifier: string) {
return /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/.test(identifier);
}

// TODO: compare function type hazards, put them into a js file and use JSDoc?
// this is a simplified version of: https://github.com/angus-c/just/blob/master/packages/collection-compare/index.mjs
function compare(
value1: unknown,
value2: unknown,
ignoreSet = new Set<string | number | symbol>(),
) {
if (Object.is(value1, value2)) {
return true;
}

if (Array.isArray(value1)) {
return compareArrays(value1, value2 as unknown[], ignoreSet);
}

if (typeof value1 === "object") {
return compareObjects(
value1 as Record<string | number | symbol, unknown>,
value2 as Record<string | number | symbol, unknown>,
ignoreSet,
);
}

return false;
}

function compareArrays(
value1: unknown[],
value2: unknown[],
ignoreSet = new Set<string | number | symbol>(),
) {
const len = value1.length;

if (len != value2.length) {
return false;
}

for (let i = 0; i < len; i++) {
if (!compare(value1[i], value2[i], ignoreSet)) {
return false;
}
}

return true;
}

function compareObjects<
T1 extends Record<string, unknown>,
T2 extends Record<string, unknown>,
>(value1: T1, value2: T2, ignoreSet = new Set<keyof T1 | keyof T2>()) {
for (const key1 of Object.keys(value1)) {
if (ignoreSet.has(key1)) {
continue;
}
if (
!(
Object.prototype.hasOwnProperty.call(value2, key1) &&
compare(value1[key1], value2[key1], ignoreSet)
)
) {
return false;
}
}

return true;
}

0 comments on commit 68567da

Please sign in to comment.