Skip to content
Merged
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
333 changes: 209 additions & 124 deletions packages/cli/lib/worker.ts

Large diffs are not rendered by default.

43 changes: 22 additions & 21 deletions packages/cli/test/cache-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ function makeContext(files: Record<string, string>) {
fileExists: n => n in files || n === realLibPath,
readFile: n => (n in files ? files[n] : (n === realLibPath ? realLibContent : undefined)),
};
return { typescript: ts, languageServiceHost: host, languageService: ts.createLanguageService(host) };
const languageService = ts.createLanguageService(host);
return { typescript: ts, program: () => languageService.getProgram()! };
}

function emptyFileCache(mtime = 0): FileCache {
Expand All @@ -62,7 +63,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());
check('syntactic rule cache entry written', !!cache.rules['syntactic']);
check('cache has 1 diagnostic', cache.rules['syntactic']?.diagnostics.length === 1);
check('hasFix false (no fix reported)', cache.rules['syntactic']?.hasFix === false);
Expand All @@ -81,7 +82,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const result = cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
const result = cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());
check('type-aware rule still produces diagnostics', result.length === 1);
check('type-aware rule NOT cached', !cache.rules['typed']);
}
Expand All @@ -100,7 +101,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program);
check('first call ran rule', runs === 1);
Expand Down Expand Up @@ -128,7 +129,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program);
check('first call ran', runs === 1);
Expand Down Expand Up @@ -156,7 +157,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());
check('post-rule cleanup deleted entry', !cache.rules['report-then-touch']);
}

Expand All @@ -179,7 +180,7 @@ function emptyFileCache(mtime = 0): FileCache {
const linter = core.createLinter(ctx, '/', config, () => []);
const cacheA = emptyFileCache(1);
const cacheB = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();
cacheFlow.lintWithCache(linter, '/a.ts', cacheA, 1, program);
cacheFlow.lintWithCache(linter, '/b.ts', cacheB, 1, program);
check('a.ts no cache entry (touched)', !cacheA.rules['sometimes-typed']);
Expand Down Expand Up @@ -221,7 +222,7 @@ function emptyFileCache(mtime = 0): FileCache {
},
},
};
const result = cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
const result = cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());
check('rule re-ran (stale cache ignored)', runs === 1);
check('result reflects fresh run', result.length === 1);
check('stale entry deleted', !cache.rules['typed']);
Expand All @@ -239,7 +240,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());
check('hasFix true after rule registered fix', cache.rules['fixable']?.hasFix === true);
}

Expand All @@ -261,7 +262,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

// First call: both run
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program);
Expand Down Expand Up @@ -294,7 +295,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program, { incremental: true, typeAwareUnaffected: true });
check('first call ran (no cache yet)', runs === 1);
Expand Down Expand Up @@ -324,7 +325,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program, { incremental: true, typeAwareUnaffected: true });
const second = cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program, {
Expand Down Expand Up @@ -358,7 +359,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

// Mode B: write the entry.
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program, { incremental: true, typeAwareUnaffected: true });
Expand All @@ -383,7 +384,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

// No options arg → mode A. Type-aware rules never cached.
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, program);
Expand Down Expand Up @@ -414,7 +415,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
const program = ctx.languageService.getProgram()!;
const program = ctx.program();

// Cold session under --incremental: no prev state, file is "affected"
// (unaffected=false). Must run AND write entry.
Expand Down Expand Up @@ -454,7 +455,7 @@ function emptyFileCache(mtime = 0): FileCache {
'/a.ts',
cache,
1,
ctx.languageService.getProgram()!,
ctx.program(),
);
check('current run returns both diagnostics', diags.length === 2);
check(
Expand Down Expand Up @@ -485,7 +486,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());

// Second run with the same mtime — rule cache-hits, only the
// persisted diagnostic comes back.
Expand All @@ -495,7 +496,7 @@ function emptyFileCache(mtime = 0): FileCache {
'/a.ts',
cache,
1,
ctx.languageService.getProgram()!,
ctx.program(),
);
check('warm cache hit returns 1 diagnostic', diags.length === 1);
check('warm replay drops the marked one', diags[0]?.messageText === 'plain');
Expand Down Expand Up @@ -528,7 +529,7 @@ function emptyFileCache(mtime = 0): FileCache {
// ── Session 1: cold, both files lint — process the early-return file
// FIRST so the rule isn't yet type-aware when its entry gets written.
const linter1 = core.createLinter(ctx, '/', config, () => []);
const program1 = ctx.languageService.getProgram()!;
const program1 = ctx.program();
const cacheSkip: FileCache = emptyFileCache(1);
const cacheCheck: FileCache = emptyFileCache(1);
cacheFlow.lintWithCache(linter1, '/skip.ts', cacheSkip, 1, program1);
Expand All @@ -551,7 +552,7 @@ function emptyFileCache(mtime = 0): FileCache {
// Both files unchanged. typeAwareUnaffected=true → both should
// cache-hit and replay cleanly.
const linter2 = core.createLinter(ctx, '/', config, () => [], ['mixed-mode']);
const program2 = ctx.languageService.getProgram()!;
const program2 = ctx.program();
let earlyReturnRanInSession2 = false;
const config2: Config = {
rules: {
Expand Down Expand Up @@ -599,7 +600,7 @@ function emptyFileCache(mtime = 0): FileCache {
};
const linter = core.createLinter(ctx, '/', config, () => []);
const cache = emptyFileCache(1);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.languageService.getProgram()!);
cacheFlow.lintWithCache(linter, '/a.ts', cache, 1, ctx.program());
// rule entry exists but with 0 diagnostics — no smuggled keys.
const json = JSON.stringify(cache);
check('NO_CACHE marker not visible in serialised cache', !json.includes('no-cache'));
Expand Down
102 changes: 102 additions & 0 deletions packages/cli/test/meta-frameworks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Smoke test for the meta-framework language-plugin path. Spawns the
// real tsslint CLI against the in-tree `fixtures/meta-frameworks` fixture
// (.tsx + .vue + .vine.ts + .astro + .mdx) for each `--*-project` flag,
// asserts the no-console rule fires on EACH framework's script content.
//
// This catches the regression class where the language plugin loads but
// the script content never makes it into the linter's program — e.g. a
// virtual-script transformation that gets undone before AST walk. Pre-
// 3.2 (LS-side `decorateLanguageServiceHost`) and post-3.2 (program-side
// host wrap) both have the same user-visible contract: `console.log`
// inside `<script>` blocks fires the rule.
//
// Run via:
// node packages/cli/test/meta-frameworks.test.js

import path = require('path');
import fs = require('fs');
import { spawnSync } from 'child_process';

const failures: string[] = [];
function check(name: string, cond: boolean, detail?: string) {
if (cond) {
process.stdout.write('.');
}
else {
failures.push(name + (detail ? ' — ' + detail : ''));
process.stdout.write('F');
}
}

const repoRoot = path.resolve(__dirname, '../../..');
const tsslintBin = path.join(repoRoot, 'packages/cli/bin/tsslint.js');
const fixtureDir = path.join(repoRoot, 'fixtures/meta-frameworks');

if (!fs.existsSync(path.join(fixtureDir, 'node_modules'))) {
console.log('meta-frameworks fixture missing node_modules; skipping.');
console.log('OK');
process.exit(0);
}

interface Pipeline {
flag: '--vue-project' | '--mdx-project' | '--astro-project' | '--vue-vine-project';
scriptFile: string;
}

const pipelines: Pipeline[] = [
// Each pipeline expects no-console to fire on at least:
// - fixture.tsx (plain TS path — sanity check, language plugin
// shouldn't break the .tsx flow)
// - the framework's own script file (proves the plugin's virtual
// script reached the AST walker)
//
// vue-vine pins to `fixture.vue` rather than `fixture.vine.ts`: the
// upstream `@vue-vine/language-service` shipped on the workspace has
// a known `vueCompilerOptions.globalTypesPath is not a function`
// crash inside its `createVirtualCode`, so .vine.ts script content
// never reaches the AST. Master has the same gap. We keep the .vue
// assertion to verify the vue-vine plugin's .vue path still works.
{ flag: '--vue-project', scriptFile: 'fixture.vue' },
{ flag: '--mdx-project', scriptFile: 'fixture.mdx' },
{ flag: '--astro-project', scriptFile: 'fixture.astro' },
{ flag: '--vue-vine-project', scriptFile: 'fixture.vue' },
];

function strip(s: string) {
// CLI output includes ANSI colour escapes; tests compare against the
// underlying text only.
return s.replace(/\[[0-9;]*m/g, '');
}

function runCli(flag: Pipeline['flag']): string {
const r = spawnSync(
process.execPath,
[tsslintBin, flag, 'tsconfig.json', '--force'],
{ cwd: fixtureDir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' },
);
return strip(r.stdout || '') + strip(r.stderr || '');
}

for (const { flag, scriptFile } of pipelines) {
const out = runCli(flag);
// Plain-TS sanity: rule fires on .tsx
check(
`${flag}: rule fires on fixture.tsx`,
out.includes('fixture.tsx') && out.includes("Calls to 'console.x' are not allowed."),
`output: ${out.slice(0, 400)}`,
);
// Language-plugin path: rule fires on the framework file
check(
`${flag}: rule fires on ${scriptFile}`,
out.includes(scriptFile) && out.includes("Calls to 'console.x' are not allowed."),
`output: ${out.slice(0, 400)}`,
);
}

process.stdout.write('\n');
if (failures.length) {
console.error(`\n${failures.length} failure(s):`);
for (const f of failures) console.error(' - ' + f);
process.exit(1);
}
console.log('OK');
Loading
Loading