shared/selectors/literal.selector.ts
shared/selectors/literal.selector.ts is a file in GovLab Extension Host. 146 lines of code and 44 definitions.
import type { AstNode, CopyOf } from "../../types/syntax.types.ts";
import {
asNode,
isType,
literalString,
nameOf,
nodeAt,
nodesAt,
propertyKeyName,
recordAt,
staticTextOf,
stringAt,
stringIn,
walk,
} from "./syntax.selector.ts";
import type { Scope } from "eslint";
const LETTERS: ReadonlySet<string> = new Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
const MIN_WORD_LETTERS = 3;
const HOLE = "…";
const hasWord = function hasWord(text: string): boolean {
let run = 0;
for (const char of text) {
run = LETTERS.has(char) ? run + 1 : 0;
if (run >= MIN_WORD_LETTERS) {
return true;
}
}
return false;
};
const templatePartsOf = function templatePartsOf(node: AstNode | null): readonly string[] {
return nodesAt(node, "quasis").map((part) => stringIn(recordAt(part, "value"), "cooked"));
};
export const templateShapeOf = function templateShapeOf(node: AstNode | null): string | null {
return isType(node, "TemplateLiteral") ? templatePartsOf(node).join(HOLE) : null;
};
const wordedTemplateOf = function wordedTemplateOf(node: AstNode | null): string | null {
if (!isType(node, "TemplateLiteral") || nodesAt(node, "expressions").length === 0) {
return null;
}
const parts = templatePartsOf(node);
return parts.some(hasWord) ? parts.join(HOLE) : null;
};
const CONCAT_OPERATOR = "+";
const concatPartsOf = function concatPartsOf(node: AstNode | null): readonly string[] | null {
if (!isType(node, "BinaryExpression") || stringAt(node, "operator") !== CONCAT_OPERATOR) {
return null;
}
const side = (part: AstNode | null): readonly string[] =>
concatPartsOf(part) ?? (isType(part, "TemplateLiteral") ? templatePartsOf(part) : [literalString(part) ?? ""]);
return [...side(nodeAt(node, "left")), ...side(nodeAt(node, "right"))];
};
const wordedConcatOf = function wordedConcatOf(node: AstNode | null): string | null {
const parts = concatPartsOf(node);
if (parts === null) {
return null;
}
return parts.some(hasWord) ? parts.join(HOLE) : null;
};
export const copyTextOf = function copyTextOf(node: AstNode | null): string | null {
return staticTextOf(node) ?? wordedTemplateOf(node) ?? wordedConcatOf(node);
};
export const boundInitOf = function boundInitOf(scope: Scope.Scope | null, name: string): AstNode | null {
for (let cursor = scope; cursor !== null; cursor = cursor.upper) {
const variable = cursor.set.get(name);
if (variable !== undefined) {
const [definition] = variable.defs;
return definition === undefined ? null : nodeAt(asNode(definition.node), "init");
}
}
return null;
};
const isCopy = function isCopy(node: AstNode | null): boolean {
const text = copyTextOf(node);
return text !== null && text.length > 0;
};
export const boundCopyOf = function boundCopyOf(scope: Scope.Scope): CopyOf {
return (node) => {
if (isCopy(node)) {
return node;
}
const bound = isType(node, "Identifier") ? boundInitOf(scope, nameOf(node)) : null;
return isCopy(bound) ? bound : null;
};
};
export const CODE_SLOTS: ReadonlySet<string> = new Set(["code", "grammar"]);
export const DIAGRAM_KINDS: ReadonlySet<string> = new Set(["diagram", "mermaid"]);
export const CODE_KINDS: ReadonlySet<string> = new Set(["code", ...DIAGRAM_KINDS]);
const KIND_SLOT = "kind";
const TEXT_SLOT = "text";
const TEXT_NODE_TYPES: ReadonlySet<string> = new Set(["Literal", "TemplateLiteral"]);
const isTextNode = function isTextNode(node: AstNode | null): node is AstNode {
return node !== null && TEXT_NODE_TYPES.has(node.type);
};
const propertyOf = function propertyOf(objectExpr: AstNode, key: string): AstNode | null {
const found = nodesAt(objectExpr, "properties").find(
(prop) => isType(prop, "Property") && propertyKeyName(prop) === key,
);
return found === undefined ? null : nodeAt(found, "value");
};
const kindedTextValue = function kindedTextValue(node: AstNode, kinds: ReadonlySet<string>): AstNode | null {
if (!isType(node, "ObjectExpression")) {
return null;
}
const kind = literalString(propertyOf(node, KIND_SLOT));
return kind !== null && kinds.has(kind) ? propertyOf(node, TEXT_SLOT) : null;
};
const codeSlotValue = function codeSlotValue(node: AstNode): AstNode | null {
if (isType(node, "Property") && CODE_SLOTS.has(propertyKeyName(node))) {
return nodeAt(node, "value");
}
return kindedTextValue(node, CODE_KINDS);
};
const diagramSlotValue = function diagramSlotValue(node: AstNode): AstNode | null {
return kindedTextValue(node, DIAGRAM_KINDS);
};
const literalsOf = function literalsOf(
program: AstNode,
slotValue: (node: AstNode) => AstNode | null,
): ReadonlySet<AstNode> {
const inline: AstNode[] = [];
const names: string[] = [];
walk(program, (node) => {
const value = slotValue(node);
if (isTextNode(value)) {
inline.push(value);
}
if (isType(value, "Identifier")) {
names.push(nameOf(value));
}
});
const named = new Set(names);
const declared: AstNode[] = [];
walk(program, (node) => {
const init = nodeAt(node, "init");
if (isType(node, "VariableDeclarator") && named.has(nameOf(nodeAt(node, "id"))) && isTextNode(init)) {
declared.push(init);
}
});
return new Set([...inline, ...declared]);
};
export const codeLiteralsOf = function codeLiteralsOf(program: AstNode): ReadonlySet<AstNode> {
return literalsOf(program, codeSlotValue);
};
export const diagramLiteralsOf = function diagramLiteralsOf(program: AstNode): ReadonlySet<AstNode> {
return literalsOf(program, diagramSlotValue);
};