mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-18 04:43:36 +00:00
140 lines
5.6 KiB
TypeScript
140 lines
5.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {
|
|
compileRExportPattern,
|
|
unescapeRString,
|
|
} from '../../src/core/ingestion/languages/r/export-pattern.js';
|
|
import { loadRPackageConfig } from '../../src/core/ingestion/languages/r/package-config.js';
|
|
import { createTempDirPool } from '../helpers/temp-dir-pool.js';
|
|
|
|
// `exportPattern("...")` arguments are POSIX (TRE) regexes; JavaScript RegExp has no
|
|
// `[:alpha:]`-style classes, so they must be translated before compiling.
|
|
describe('compileRExportPattern', () => {
|
|
const matches = (pattern: string, name: string): boolean =>
|
|
compileRExportPattern(pattern)?.test(name) ?? false;
|
|
|
|
it('matches the RStudio default pattern against an ordinary name', () => {
|
|
expect(matches('^[[:alpha:]]+', 'tidy_scores')).toBe(true);
|
|
expect(matches('^[[:alpha:]]+', '.hidden')).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
['[[:alpha:]]', 'q', '5'],
|
|
['[[:alnum:]]', '7', '_'],
|
|
['[[:digit:]]', '3', 'x'],
|
|
['[[:upper:]]', 'Q', 'q'],
|
|
['[[:lower:]]', 'q', 'Q'],
|
|
['[[:punct:]]', '.', 'a'],
|
|
['[[:space:]]', ' ', 'a'],
|
|
])('translates %s', (pattern, hit, miss) => {
|
|
expect(matches(`^${pattern}$`, hit)).toBe(true);
|
|
expect(matches(`^${pattern}$`, miss)).toBe(false);
|
|
});
|
|
|
|
it('treats punct as ASCII punctuation including brackets, backslash, backtick and tilde', () => {
|
|
for (const ch of ['!', '/', ':', '@', '[', '\\', ']', '^', '_', '`', '{', '~']) {
|
|
expect(matches('^[[:punct:]]$', ch)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('translates a negated class', () => {
|
|
expect(matches('^[^[:digit:]]+$', 'abc')).toBe(true);
|
|
expect(matches('^[^[:digit:]]+$', 'ab1')).toBe(false);
|
|
});
|
|
|
|
it('combines a class with a range and literals in one bracket expression', () => {
|
|
expect(matches('^[[:digit:]a-c_]+$', '1a_c2')).toBe(true);
|
|
expect(matches('^[[:digit:]a-c_]+$', '1d')).toBe(false);
|
|
});
|
|
|
|
it('combines several classes in one bracket expression', () => {
|
|
expect(matches('^[[:upper:][:digit:]]+$', 'AB12')).toBe(true);
|
|
expect(matches('^[[:upper:][:digit:]]+$', 'Ab12')).toBe(false);
|
|
});
|
|
|
|
it('honours ^ and $ anchors around a class', () => {
|
|
expect(matches('^[[:alpha:]]', '9a')).toBe(false);
|
|
expect(matches('[[:digit:]]$', 'a9')).toBe(true);
|
|
expect(matches('[[:digit:]]$', '9a')).toBe(false);
|
|
expect(matches('[[:digit:]]', 'a9b')).toBe(true);
|
|
});
|
|
|
|
it('translates a class in a pattern with several bracket expressions', () => {
|
|
expect(matches('^[[:upper:]][a-z]+[[:digit:]]*$', 'Foo12')).toBe(true);
|
|
expect(matches('^[[:upper:]][a-z]+[[:digit:]]*$', 'foo12')).toBe(false);
|
|
});
|
|
|
|
it('leaves patterns without POSIX classes unchanged', () => {
|
|
expect(compileRExportPattern('^[a-z]')?.source).toBe('^[a-z]');
|
|
expect(compileRExportPattern('^Pattern')?.source).toBe('^Pattern');
|
|
expect(matches('^Pattern', 'PatternX')).toBe(true);
|
|
expect(matches('^Pattern', 'xPattern')).toBe(false);
|
|
});
|
|
|
|
it('does not treat an escaped bracket as a bracket expression', () => {
|
|
expect(matches('^\\[[:alpha:]$', '[:alpha:')).toBe(false);
|
|
});
|
|
|
|
it('never throws and yields null for an uncompilable pattern', () => {
|
|
for (const bad of ['(', '[abc', '[[:alpha:]', '[[:nosuch:]]', '*', '[[:alpha:', '\\']) {
|
|
expect(() => compileRExportPattern(bad)).not.toThrow();
|
|
expect(compileRExportPattern(bad)).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('loadRPackageConfig -> RNamespaceInfo.exportPatterns (POSIX classes)', () => {
|
|
const pool = createTempDirPool('gn-r-exp-');
|
|
|
|
const load = async (namespace: string) => {
|
|
const root = pool.dir();
|
|
fs.writeFileSync(path.join(root, 'DESCRIPTION'), 'Package: rootpkg\n');
|
|
fs.writeFileSync(path.join(root, 'NAMESPACE'), namespace);
|
|
const config = await loadRPackageConfig(root);
|
|
return config?.namespaceInfoByPackageDir.get('')?.exportPatterns ?? [];
|
|
};
|
|
|
|
it('compiles the RStudio default exportPattern so it matches tidy_scores', async () => {
|
|
const patterns = await load('exportPattern("^[[:alpha:]]+")\n');
|
|
expect(patterns).toHaveLength(1);
|
|
expect(patterns.some((p) => p.test('tidy_scores'))).toBe(true);
|
|
});
|
|
|
|
it('drops an invalid pattern without throwing and keeps the valid ones', async () => {
|
|
const patterns = await load('exportPattern("[[:nosuch:]]")\nexportPattern("^[[:digit:]]")\n');
|
|
expect(patterns).toHaveLength(1);
|
|
expect(patterns[0].test('1a')).toBe(true);
|
|
});
|
|
|
|
it('keeps a plain pattern working', async () => {
|
|
const patterns = await load('exportPattern("^Pattern")\n');
|
|
expect(patterns.some((p) => p.test('PatternX'))).toBe(true);
|
|
expect(patterns.some((p) => p.test('xPattern'))).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('unescapeRString', () => {
|
|
it('returns text without a backslash unchanged', () => {
|
|
expect(unescapeRString('^[[:alpha:]]+')).toBe('^[[:alpha:]]+');
|
|
});
|
|
|
|
it('handles the common single-character escapes', () => {
|
|
expect(unescapeRString('a\\\\b')).toBe('a\\b');
|
|
expect(unescapeRString('\\"\\\'\\`')).toBe('"\'`');
|
|
expect(unescapeRString('\\n\\t\\r\\a\\b\\f\\v')).toBe('\n\t\r\x07\b\f\v');
|
|
});
|
|
|
|
it('handles hex and unicode escapes, with and without braces', () => {
|
|
expect(unescapeRString('\\x41\\u0042\\u{43}\\U00000044\\U{45}')).toBe('ABCDE');
|
|
expect(unescapeRString('\\U{1F600}')).toBe('\u{1F600}');
|
|
});
|
|
|
|
it('keeps an unknown or malformed escape verbatim and never throws', () => {
|
|
expect(unescapeRString('\\.')).toBe('\\.');
|
|
expect(unescapeRString('\\xZZ')).toBe('\\xZZ');
|
|
expect(unescapeRString('\\u{110000}')).toBe('\\u{110000}');
|
|
expect(unescapeRString('\\u{41')).toBe('\\u{41');
|
|
expect(unescapeRString('trailing\\')).toBe('trailing\\');
|
|
});
|
|
});
|