Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active September 24, 2021 22:22
Show Gist options
  • Save mizchi/b801edd949d9c6276a85eaffefe32b58 to your computer and use it in GitHub Desktop.
Save mizchi/b801edd949d9c6276a85eaffefe32b58 to your computer and use it in GitHub Desktop.
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));
@mizchi
Copy link
Author

mizchi commented Sep 24, 2021

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