Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
],
"scripts": {
"build": "tsc -b",
"flint": "node packages/flint/bin/index.js",
"flint": "flint",
"lint": "eslint . --max-warnings 0",
"lint:knip": "knip",
"lint:md": "markdownlint \"**/*.md\" \".github/**/*.md\" --rules sentences-per-line",
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/runCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { createRendererFactory } from "./renderers/createRendererFactory.js";
import { runCliOnce } from "./runCliOnce.js";
import { runCliWatch } from "./runCliWatch.js";

export async function runCli() {
export async function runCli(args: string[]) {
const { values } = parseArgs({
args,
options,
strict: true,
});
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,19 @@ export { globs } from "./globs/index.js";
export { createLanguage } from "./languages/createLanguage.js";
export { createPlugin } from "./plugins/createPlugin.js";
export { formatReportPrimary } from "./reporting/formatReportPrimary.js";
export { computeRulesWithOptions } from "./running/computeRulesWithOptions.js";
export { lintFixing } from "./running/lintFixing.js";
export { lintOnce } from "./running/lintOnce.js";
export {
setTSExtraSupportedExtensions,
setTSProgramCreationProxy,
} from "./ts-patch/proxy-program.js";
export * from "./types/about.js";
export * from "./types/arrays.js";
export * from "./types/cache.js";
export * from "./types/changes.js";
export * from "./types/configs.js";
export * from "./types/context.js";
export * from "./types/directives.js";
export * from "./types/formatting.js";
export * from "./types/languages.js";
Expand All @@ -26,6 +33,6 @@ export * from "./types/ranges.js";
export * from "./types/reports.js";
export * from "./types/rules.js";
export * from "./types/shapes.js";
export { binarySearch } from "./utils/arrays.js";
export * from "./utils/arrays.js";
export * from "./utils/getColumnAndLineOfPosition.js";
export * from "./utils/predicates.js";
20 changes: 20 additions & 0 deletions packages/core/src/ts-patch/install-patch-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { registerHooks } from "node:module";

import { transformTscContent } from "./shared.js";

const typescriptUrl = import.meta.resolve("typescript");

registerHooks({
load(url, context, nextLoad) {
const next = nextLoad(url, context);

if (url !== typescriptUrl || next.source == null) {
return next;
}

return {
...next,
source: transformTscContent(next.source.toString()),
};
},
});
19 changes: 19 additions & 0 deletions packages/core/src/ts-patch/install-patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import fs from "node:fs";
import { createRequire } from "node:module";

import { transformTscContent } from "./shared.js";

const require = createRequire(import.meta.url);
const typescriptPath = require.resolve("typescript");

const origReadFileSync = fs.readFileSync;
// @ts-expect-error
fs.readFileSync = (...args) => {
const res = origReadFileSync(...args);
if (args[0] === typescriptPath) {
return transformTscContent(res.toString());
}
return res;
};
require("typescript");
fs.readFileSync = origReadFileSync;
54 changes: 54 additions & 0 deletions packages/core/src/ts-patch/proxy-program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { createProgram } from "typescript";

const globalTyped = globalThis as unknown as {
createProgramProxies: Set<
(
ts: typeof import("typescript"),
create: typeof createProgram,
) => typeof createProgram
>;
extraSupportedExtensions: Set<string>;
};
// Since it's not possible to change the module graph evaluation order,
// we store proxies unordered
globalTyped.createProgramProxies ??= new Set();

globalTyped.extraSupportedExtensions ??= new Set();

export function getExtraSupportedExtensions() {
return Array.from(globalTyped.extraSupportedExtensions);
}

export function setTSExtraSupportedExtensions(extensions: string[]) {
for (const ext of extensions) {
globalTyped.extraSupportedExtensions.add(ext);
}
return () => {
for (const ext of extensions) {
globalTyped.extraSupportedExtensions.delete(ext);
}
};
}

export function setTSProgramCreationProxy(
proxy: (
ts: typeof import("typescript"),
create: typeof createProgram,
) => typeof createProgram,
) {
globalTyped.createProgramProxies.add(proxy);

return () => globalTyped.createProgramProxies.delete(proxy);
}

// TODO: explanation
export function proxyCreateProgram(
ts: typeof import("typescript"),
original: typeof createProgram,
) {
let proxied = original;
for (const proxy of globalTyped.createProgramProxies) {
proxied = proxy(ts, proxied);
}
return proxied;
}
89 changes: 89 additions & 0 deletions packages/core/src/ts-patch/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { fileURLToPath } from "node:url";

function replaceOrThrow(
source: string,
search: RegExp | string,
replace: (substring: string, ...args: any[]) => string,
): string {
const before = source;
source = source.replace(search, replace);
const after = source;
if (after === before) {
throw new Error("Flint bug: failed to replace: " + search.toString());
}
return after;
}

const coreCreateProxyProgramPath = fileURLToPath(
import.meta.resolve("./proxy-program.js"),
);

// https://github.com/volarjs/volar.js/blob/e08f2f449641e1c59686d3454d931a3c29ddd99c/packages/typescript/lib/quickstart/runTsc.ts
export function transformTscContent(source: string): string {
source += `
function _flintDynamicProxy(getter) {
return new Proxy(function () {}, new Proxy({}, {
get(_, property) {
return (_, ...args) => Reflect[property](getter(), ...args)
}
}))
}

const _flintTsPatch = require(${JSON.stringify(coreCreateProxyProgramPath)})
`;
injectExtraSupportedExtensions("supportedTSExtensions");
injectExtraSupportedExtensions("supportedJSExtensions");
injectExtraSupportedExtensions("allSupportedExtensions");

injectDynamicProxy("supportedTSExtensionsFlat");
injectDynamicProxy("supportedTSExtensionsWithJson");
injectDynamicProxy("supportedJSExtensionsFlat");
injectDynamicProxy("allSupportedExtensionsWithJson");

source = replaceOrThrow(
source,
"function changeExtension(path, newExtension)",
(s) => `${s} {
return _flintTsPatch.getExtraSupportedExtensions().some(ext => path.endsWith(ext))
? path + newExtension
: _changeExtension(path, newExtension)
}

function _changeExtension(path, newExtension)`,
);

source = replaceOrThrow(
source,
/function createProgram\(/,
(match, args: string) => `function createProgram(...args) {
return _flintTsPatch.proxyCreateProgram(
new Proxy({}, { get(_target, p, _receiver) { return eval(p); } } ),
_createProgram,
)(...args)
}

function _createProgram(`,
);

return source;

function injectExtraSupportedExtensions(variable: string) {
injectDynamicProxy(
variable,
(initializer) =>
`${initializer}.map((group, i) => (i === 0 && group.push(..._flintTsPatch.getExtraSupportedExtensions()), group))`,
);
}

function injectDynamicProxy(
variable: string,
transformInitializer?: (initializer: string) => string,
) {
source = replaceOrThrow(
source,
new RegExp(`(${variable}) = (.*)(?=;)`),
(match, decl: string, initializer: string) =>
`${decl} = _flintDynamicProxy(() => ${transformInitializer?.(initializer) ?? initializer})`,
);
}
}
7 changes: 6 additions & 1 deletion packages/core/src/types/ranges.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The column and line of a character in a source file, as visualized to users.
*/
export interface ColumnAndLine {
export interface ColumnAndLineWithoutRaw {
/**
* Column in a source file (0-indexed integer).
*/
Expand All @@ -11,7 +11,12 @@ export interface ColumnAndLine {
* Line in a source file (0-indexed integer).
*/
line: number;
}

/**
* The column and line of a character in a source file, as visualized to users.
*/
export interface ColumnAndLine extends ColumnAndLineWithoutRaw {
/**
* The original raw character position in the source file (0-indexed integer).
*/
Expand Down
43 changes: 42 additions & 1 deletion packages/core/src/utils/getColumnAndLineOfPosition.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { describe, expect, test, vi } from "vitest";

import { getColumnAndLineOfPosition } from "./getColumnAndLineOfPosition.js";
import {
getColumnAndLineOfPosition,
getPositionOfColumnAndLine,
} from "./getColumnAndLineOfPosition.js";

describe("getColumnAndLineOfPosition", () => {
test("negative position", () => {
Expand Down Expand Up @@ -260,3 +263,41 @@ describe("getColumnAndLineOfPosition", () => {
});
});
});

describe("getPositionOfColumnAndLine", () => {
test("clamps negative line", () => {
const res = getPositionOfColumnAndLine("012", { line: -1, column: 1 });

expect(res).toBe(1);
});

test("clamps line after EOF", () => {
const res = getPositionOfColumnAndLine("012", { line: 1, column: 1 });

expect(res).toBe(1);
});

test("clamps column", () => {
const res = getPositionOfColumnAndLine("012\n45", { line: 0, column: 4 });

expect(res).toBe(3);
});

test("clamps column with empty line before it", () => {
const res = getPositionOfColumnAndLine("012\n\n56", { line: 1, column: 5 });

expect(res).toBe(4);
});

test("clamps column on the last line to EOF", () => {
const res = getPositionOfColumnAndLine("012", { line: 1, column: 5 });

expect(res).toBe(3);
});

test("column on EOF", () => {
const res = getPositionOfColumnAndLine("012", { line: 1, column: 3 });

expect(res).toBe(3);
});
});
39 changes: 38 additions & 1 deletion packages/core/src/utils/getColumnAndLineOfPosition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ColumnAndLine } from "../types/ranges.js";
import { ColumnAndLine, ColumnAndLineWithoutRaw } from "../types/ranges.js";
import { binarySearch } from "./arrays.js";

/** Subset of ts.SourceFileLike */
Expand Down Expand Up @@ -122,3 +122,40 @@ function computeLineStarts(source: string): readonly number[] {
res.push(lineStart);
return res;
}

/**
* Prefer passing a `source` of type `HasGetLineAndCharacterOfPosition` or `SourceFileWithLineMap`.
* This way, the expensive computation of the `lineMap` will be cached across multiple calls.
*/
export function getPositionOfColumnAndLine(
source: SourceFileWithLineMap | string,
columnAndLine: ColumnAndLineWithoutRaw,
): number {
if (typeof source === "string") {
return computePositionOfColumnAndLine(
source,
computeLineStarts(source),
columnAndLine,
);
}
source.lineMap ??= computeLineStarts(source.text);
return computePositionOfColumnAndLine(
source.text,
source.lineMap,
columnAndLine,
);
}

function computePositionOfColumnAndLine(
sourceText: string,
lineStarts: readonly number[],
{ column, line }: ColumnAndLineWithoutRaw,
): number {
line = Math.min(Math.max(line, 0), lineStarts.length - 1);

const res = lineStarts[line] + column;
if (line === lineStarts.length - 1) {
return Math.min(res, sourceText.length);
}
return Math.min(res, lineStarts[line + 1] - 1);
}
4 changes: 3 additions & 1 deletion packages/flint/bin/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env node
import { runCli } from "@flint.fyi/cli";
// @ts-check
import { enableCompileCache } from "node:module";

enableCompileCache();

await import("@flint.fyi/core/lib/ts-patch/install-patch.js");
const { runCli } = await import("@flint.fyi/cli");
process.exitCode = await runCli(process.argv.slice(2));
2 changes: 0 additions & 2 deletions packages/plugin-browser/src/rules/keyboardEventKeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ document.addEventListener("keyup", (event: KeyboardEvent) => {
console.log(event.which);
});
`,
// only: true,
snapshot: `
document.addEventListener("keyup", (event: KeyboardEvent) => {
console.log(event.which);
Expand All @@ -86,7 +85,6 @@ function handleKeyDown(event: KeyboardEvent) {
return event.keyCode === 8;
}
`,
// only: true,
snapshot: `
function handleKeyDown(event: KeyboardEvent) {
return event.keyCode === 8;
Expand Down
Loading