mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
ci: distinguish indirect unit inputs from comments and data (#31876)
This commit is contained in:
@@ -2,6 +2,7 @@ import { globSync, readFileSync } from 'node:fs'
|
||||
import { posix, join } from 'node:path'
|
||||
import ts from 'typescript-api'
|
||||
import { createWorkspaceSourceResolver } from './workspace-source-exports.mjs'
|
||||
import { hasIndirectUnitInputs } from './ci-unit-indirect-inputs.mjs'
|
||||
|
||||
const EXTENSIONS = [
|
||||
'',
|
||||
@@ -15,8 +16,6 @@ const EXTENSIONS = [
|
||||
'/index.tsx',
|
||||
'/index.js'
|
||||
]
|
||||
const INDIRECT_INPUT =
|
||||
/\b(?:readFile\w*|readdir\w*|glob\w*|spawn\w*|execFile\w*|execSync|runProcess\w*|fork|Worker|createRequire|getBuiltinModule)\b|\bimport\s*\(\s*[^'"\s]|\brequire\s*\(\s*[^'"\s]|\bnew\s+URL\s*\(/
|
||||
|
||||
function localPath(file, specifier) {
|
||||
if (specifier.startsWith('.')) {
|
||||
@@ -41,10 +40,15 @@ export function buildUnitDependencyGraph(sources, workspace = { resolve: () => n
|
||||
reverse.get(input).add(file)
|
||||
}
|
||||
for (const [file, source] of sources) {
|
||||
if (INDIRECT_INPUT.test(source) || file.startsWith('config/') || file.startsWith('tests/')) {
|
||||
const imports = ts.preProcessFile(source, true, true).importedFiles
|
||||
if (
|
||||
file.startsWith('config/') ||
|
||||
file.startsWith('tests/') ||
|
||||
hasIndirectUnitInputs(file, source, imports)
|
||||
) {
|
||||
opaque.add(file)
|
||||
}
|
||||
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
|
||||
for (const imported of imports) {
|
||||
if (imported.fileName === 'module' || imported.fileName === 'node:module') {
|
||||
opaque.add(file)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import ts from 'typescript-api'
|
||||
import { literalString, propertyName, unwrap } from './static-module-specifiers.mjs'
|
||||
|
||||
const INDIRECT_NAME =
|
||||
/^(?:readFile\w*|readdir\w*|glob(?:Sync|Stream|Iterate|SyncIterate|StreamSync)?|spawn(?:Sync)?|execFile(?:Sync)?|execSync|runProcess\w*|fork|Worker|createRequire|getBuiltinModule|eval|Function)$/
|
||||
const INDIRECT_CANDIDATE = new RegExp(
|
||||
`\\b${INDIRECT_NAME.source.slice(1, -1)}\\b|\\b(?:import|require)\\s*\\(|\\bURL\\b`
|
||||
)
|
||||
const INDIRECT_MODULES = new Set([
|
||||
'fs',
|
||||
'node:fs',
|
||||
'fs/promises',
|
||||
'node:fs/promises',
|
||||
'module',
|
||||
'node:module',
|
||||
'child_process',
|
||||
'node:child_process',
|
||||
'worker_threads',
|
||||
'node:worker_threads',
|
||||
'vm',
|
||||
'node:vm',
|
||||
'glob',
|
||||
'fast-glob',
|
||||
'globby',
|
||||
'tinyglobby'
|
||||
])
|
||||
|
||||
export function hasIndirectUnitInputs(file, contents, imports) {
|
||||
if (file.endsWith('.json')) {
|
||||
return false
|
||||
}
|
||||
if (imports.some(({ fileName }) => INDIRECT_MODULES.has(fileName))) {
|
||||
return true
|
||||
}
|
||||
if (!INDIRECT_CANDIDATE.test(contents)) {
|
||||
return false
|
||||
}
|
||||
const source = ts.createSourceFile(file, contents, ts.ScriptTarget.Latest, false)
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
return true
|
||||
}
|
||||
function visit(node) {
|
||||
if (
|
||||
ts.isTypeNode(node) ||
|
||||
ts.isInterfaceDeclaration(node) ||
|
||||
ts.isTypeAliasDeclaration(node) ||
|
||||
((ts.isImportClause(node) ||
|
||||
ts.isImportSpecifier(node) ||
|
||||
ts.isExportDeclaration(node) ||
|
||||
ts.isExportSpecifier(node)) &&
|
||||
node.isTypeOnly)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (ts.isIdentifier(node) && INDIRECT_NAME.test(node.text)) {
|
||||
return true
|
||||
}
|
||||
if (ts.isElementAccessExpression(node) && INDIRECT_NAME.test(propertyName(node) ?? '')) {
|
||||
return true
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = unwrap(node.expression)
|
||||
if (
|
||||
(callee.kind === ts.SyntaxKind.ImportKeyword ||
|
||||
(ts.isIdentifier(callee) && callee.text === 'require')) &&
|
||||
literalString(node.arguments[0]) === null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (
|
||||
ts.isNewExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === 'URL'
|
||||
) {
|
||||
const url = literalString(node.arguments?.[0])
|
||||
if (url === null || !/^(?:https?|wss?):/.test(url)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return ts.forEachChild(node, visit) ?? false
|
||||
}
|
||||
return visit(source)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
|
||||
import { selectUnitFiles } from './ci-unit-selection.mjs'
|
||||
|
||||
function selected(source) {
|
||||
const sources = new Map([
|
||||
['src/changed.ts', 'export const value = 1'],
|
||||
['src/changed.test.ts', "import './changed'"],
|
||||
['src/indirect.ts', source],
|
||||
['src/indirect.test.ts', "import './indirect'"]
|
||||
])
|
||||
const graph = buildUnitDependencyGraph(sources)
|
||||
return selectUnitFiles(['src/changed.test.ts', 'src/indirect.test.ts'], ['src/changed.ts'], {
|
||||
...graph,
|
||||
files: new Set(sources.keys())
|
||||
}).files
|
||||
}
|
||||
|
||||
it.each([
|
||||
'// spawn a process, readFileSync, glob, new URL(path)',
|
||||
'export const value = globalThis.value ?? global.value',
|
||||
"export const prose = 'spawn readFileSync glob'",
|
||||
'export interface Config { spawnToken: string; Worker: string }',
|
||||
'export type Config = { readFile: string; global: boolean }',
|
||||
"export const url = new URL('https://example.test/data')",
|
||||
'export const spawnToken = "opaque-token"'
|
||||
])('does not retain unrelated tests for syntax that loads no repository inputs: %s', (source) => {
|
||||
expect(selected(source)).toEqual(['src/changed.test.ts'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
"import { readFileSync as read } from 'node:fs'; export const value = read(path)",
|
||||
"import * as fs from 'node:fs'; export const value = fs['read' + 'FileSync'](path)",
|
||||
"import { spawn as launch } from 'node:child_process'; launch(binary)",
|
||||
"import { glob as find } from 'tinyglobby'; find(pattern)",
|
||||
"import * as vm from 'node:vm'; vm['run' + 'InNewContext'](code)",
|
||||
"import * as hostModule from 'node:module'; export const loader = hostModule['create' + 'Require'](path)",
|
||||
"const file = new URL('./fixture.txt', import.meta.url)",
|
||||
'const url = new URL(runtimeValue)',
|
||||
'import(computedModule)',
|
||||
'require(computedModule)',
|
||||
"const read = fs['readFileSync']; read(path)",
|
||||
'const execute = runProcess; execute(command)',
|
||||
'const load = process.getBuiltinModule; load(moduleName)',
|
||||
'eval(source)',
|
||||
'const execute = new Function(source); execute()',
|
||||
'spawn( @@@'
|
||||
])(
|
||||
'retains indirect consumers even for aliases, computed access and invalid syntax: %s',
|
||||
(source) => {
|
||||
expect(selected(source)).toEqual(['src/changed.test.ts', 'src/indirect.test.ts'])
|
||||
}
|
||||
)
|
||||
|
||||
it('retains type-import ownership edges independently of runtime opacity', () => {
|
||||
const graph = buildUnitDependencyGraph(
|
||||
new Map([
|
||||
['src/types.ts', 'export interface Worker { id: string }'],
|
||||
['src/consumer.ts', "import type { Worker } from './types'"],
|
||||
['src/value.json', '{"global":true}']
|
||||
])
|
||||
)
|
||||
expect([...graph.reverse.get('src/types.ts')]).toEqual(['src/consumer.ts'])
|
||||
expect(graph.opaque.size).toBe(0)
|
||||
})
|
||||
Reference in New Issue
Block a user