Last active
September 24, 2021 22:22
-
-
Save mizchi/b801edd949d9c6276a85eaffefe32b58 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ts from "typescript"; | |
type Files = { [key: string]: string }; | |
type SourceFiles = { [key: string]: ts.SourceFile | undefined }; | |
function getDiagnostics(rawFiles: Files, root: string[]) { | |
const compiledFiles: SourceFiles = {}; | |
const options: ts.CompilerOptions = { | |
target: ts.ScriptTarget.ESNext, | |
}; | |
const host: ts.CompilerHost = { | |
fileExists: (filePath) => !!rawFiles[filePath], | |
directoryExists: (dirPath) => dirPath === "/", | |
getCurrentDirectory: () => "/", | |
getDirectories: () => [], | |
getCanonicalFileName: (fileName) => fileName, | |
getNewLine: () => "\n", | |
getDefaultLibFileName: () => { | |
return "/_libs/lib.esnext.d.ts"; | |
}, | |
getSourceFile: (filePath) => { | |
// compile on demand | |
const compiled = compiledFiles[filePath]; | |
if (compiled) return compiled; | |
compiledFiles[filePath] = ts.createSourceFile( | |
filePath, | |
rawFiles[filePath], | |
ts.ScriptTarget.Latest | |
); | |
return compiledFiles[filePath]; | |
}, | |
readFile: (filePath) => { | |
// console.log("read", filePath); | |
return rawFiles[filePath]; | |
}, | |
useCaseSensitiveFileNames: () => true, | |
writeFile: (fileName, data) => { | |
console.log("ts write >", fileName, data); | |
}, | |
}; | |
const program = ts.createProgram({ | |
options, | |
rootNames: root, | |
host, | |
}); | |
return ts.getPreEmitDiagnostics(program); | |
} | |
import fs from "fs/promises"; | |
import path from "path"; | |
import zlib from "zlib"; | |
const tsSourcePath = path.join(__dirname, "../node_modules/typescript/lib/"); | |
async function readLibFiles(): Promise<Files> { | |
const libFiles = await fs.readdir(tsSourcePath); | |
const entries = await Promise.all( | |
libFiles | |
.filter((file) => file.startsWith("lib.") && file.endsWith(".d.ts")) | |
.map(async (f) => { | |
const content = await fs.readFile(path.join(tsSourcePath, f), "utf8"); | |
return [`/_libs/${f}`, content]; | |
}) | |
); | |
return Object.fromEntries(entries); | |
} | |
const dumpPath = path.join(__dirname, "libs.json.gz"); | |
async function dumpLibsFile(libFiles: Files) { | |
const zipped = zlib.gzipSync(JSON.stringify(libFiles)); | |
await fs.writeFile(dumpPath, zipped); | |
console.log("gen >", "libs.json.gz", zipped.length); | |
} | |
async function loadLibsFile(): Promise<Files> { | |
const buf = await fs.readFile(dumpPath); | |
const text = zlib.gunzipSync(buf).toString("utf-8"); | |
return JSON.parse(text); | |
} | |
async function main() { | |
const libFiles = await readLibFiles(); | |
// await dumpLibsFile(libFiles); | |
// from saved | |
// const libFiles = await loadLibsFile(); | |
const rawFiles: Files = { | |
"/file.ts": "export const x: number = false", | |
"/input.ts": `import { x } from "./file"; 1 as false;`, | |
}; | |
const ret = getDiagnostics({ ...libFiles, ...rawFiles }, ["/input.ts"]); | |
for (const d of ret) { | |
// @ts-ignore | |
console.log(d.file?.fileName, d.messageText); | |
} | |
} | |
main().catch((e) => console.error(e)); |
Use case: check deno permission subset
import { readLibFiles, getDiagnostics, Files } from "./ts-diag";
type PermExpr = string[] | "ALLOW" | "DENY";
type ParsedPermArgs = {
"allow-read"?: PermExpr;
"allow-write"?: PermExpr;
"allow-net"?: PermExpr;
"allow-run"?: PermExpr;
"allow-hrtime"?: boolean;
"allow-ffi"?: boolean;
};
const exprToString = (expr: PermExpr | undefined): string => {
if (expr == null) return "DENY";
if (expr instanceof Array) {
return expr.map((x) => `"${x}"`).join(" | ");
}
return expr;
};
const buildPermCheckCode = (root: ParsedPermArgs, sub: ParsedPermArgs) => {
const subPermcode = Object.entries(sub).reduce((acc: string, [key, val]) => {
if (typeof val === "boolean") {
return acc + ` "${key}": ${val ?? "DENY"}\n`;
}
return acc + ` "${key}": ${exprToString(val)}\n`;
}, "");
// compile to this
const rootPermCode = `Permission<
// ALLOW_READ
${exprToString(root["allow-read"])},
// ALLOW_WRITE
${exprToString(root["allow-write"])},
// ALLOW_NET,
${exprToString(root["allow-net"])},
// ALLOW_RUN,
${exprToString(root["allow-run"])},
// ALLOW_HRTIME
${root["allow-hrtime"] || "DENY"},
// ALLOW_FFI
${root["allow-ffi"] || "DENY"}
>`;
return `
// ALLOW can cast DENY. DENY can not cast ALLOW.
type ALLOW = boolean;
type DENY = false;
// Access Pattern
type AccessExpr<T extends string> = T | ALLOW | DENY;
type Permission<
AllowRead extends AccessExpr<string>,
AllowWrite extends AccessExpr<string>,
AllowNet extends AccessExpr<string>,
AllowRun extends AccessExpr<string>,
AllowHrtime extends boolean,
AllowFfi extends boolean,
> = {
"allow-read": AllowRead,
"allow-write": AllowWrite,
"allow-net": AllowNet,
"allow-run": AllowRun,
"allow-hrtime": AllowHrtime,
"allow-ffi": AllowFfi,
}
type RootPermission = ${rootPermCode};
interface MyPermission extends RootPermission {
${subPermcode}
}
`;
};
async function isValidSubsetPermission(
root: ParsedPermArgs,
sub: ParsedPermArgs
): Promise<boolean> {
const libFiles = await readLibFiles();
// await dumpLibsFile(libFiles);
// from saved
// const libFiles = await loadLibsFile();
const rawFiles: Files = {
"/input.ts": buildPermCheckCode(
{
"allow-read": ["./b"],
"allow-write": "DENY",
},
{
"allow-read": ["./b"],
"allow-write": "ALLOW",
}
),
};
const ret = getDiagnostics({ ...libFiles, ...rawFiles }, ["/input.ts"]);
for (const d of ret) {
console.log(d.file?.fileName, d.messageText);
}
return ret.length === 0;
}
async function main() {
const isValid = await isValidSubsetPermission(
{
"allow-read": ["./b", "./a"],
"allow-write": "DENY",
},
{
"allow-read": ["./b"],
"allow-write": "ALLOW",
}
);
console.log("isValid", isValid);
}
main().catch((e) => console.error(e));
Result
/input.ts {
messageText: "Interface 'MyPermission' incorrectly extends interface 'RootPermission'.",
category: 1,
code: 2430,
next: [
{
messageText: `Types of property '"allow-write"' are incompatible.`,
category: 1,
code: 2326,
next: [Array]
}
]
}
isValid false
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Result