fix(row 333 follow-ups R3): a spread before import is a dynamic import (Vikunja #779)
The negative lookbehind in DYNAMIC and in the dynamic branch of IMPORT excluded any dot
before `import`. That also skipped the last dot of the spread punctuator, so
`${({...import('../x.mjs')})}` returned [] for an existing, a missing and an outside
target, and `{...import(name)}` in plain code was neither returned nor refused.
Both patterns now share one fragment: a word character or $ before `import`, or a single
dot (x.import, x?.import), makes it a property call; a dot that follows another dot ends a
spread and does not exclude it. `1..import(` is read as an import too, which errs toward
refusal; the header names it.
Tests: a spread inside a substitution refuses for an existing, a missing and an outside
target and for array spread; a spread in plain code is returned, reaches the target
checks, and is refused when its argument is not a plain string; x.import and x?.import
stay property calls in plain code and in a substitution.
This commit is contained in:
1 parent
42c72d781b
commit
180fb8ab00
2 files changed
+71
-8
No files matched your search
@@ -7,12 +7,14 @@
|
||||
// Vikunja #779: the helper reads imports with a regex, not a parser, so it stays strict about what
|
||||
// it cannot read. Comments are blanked before matching, so an import in a comment is not one, and an
|
||||
// import form inside a string or a regular expression is not one either. A dynamic import written
|
||||
// with a space, `import ('./x.mjs')`, counts. These throw: a dynamic import whose argument is not a
|
||||
// plain string, a dynamic import inside a template substitution (${...}), a file with an unterminated
|
||||
// string, comment, template or regular expression, a target that is missing or not a regular file,
|
||||
// and a target whose real path leaves the repository. A substitution with no dynamic import is
|
||||
// blanked with the rest of the template, as is `${import.meta.url}`; the text of a template and a
|
||||
// substitution that holds a template of its own are read to their ends.
|
||||
// with a space, `import ('./x.mjs')`, counts, and so does one after a spread, `{...import('./x.mjs')}`;
|
||||
// a property call such as `x.import(...)` or `x?.import(...)` does not. `1..import(...)`, a property
|
||||
// call on a number, reads as an import too, which errs toward refusal. These throw: a dynamic import
|
||||
// whose argument is not a plain string, a dynamic import inside a template substitution (${...}), a
|
||||
// file with an unterminated string, comment, template or regular expression, a target that is missing
|
||||
// or not a regular file, and a target whose real path leaves the repository. A substitution with no
|
||||
// dynamic import is blanked with the rest of the template, as is `${import.meta.url}`; the text of a
|
||||
// template and a substitution that holds a template of its own are read to their ends.
|
||||
// The scanner knows line and block comments, quoted strings, template literals, regular expression
|
||||
// literals (by the usual guess from the previous token) and a hashbang. The guess is wrong for a
|
||||
// regular expression after `)` or a keyword, as in `return /x/.test(y)`. After `++` or `--` a slash
|
||||
@@ -23,8 +25,16 @@
|
||||
import {readdirSync,readFileSync,realpathSync,statSync} from 'node:fs';
|
||||
import {dirname,join,relative,resolve,sep} from 'node:path';
|
||||
|
||||
const IMPORT=/(?:import|export)\s[^'"`;]*?\bfrom\s*['"](\.{1,2}\/[^'"]+)['"]|\bimport\s*['"](\.{1,2}\/[^'"]+)['"]|(?<![.\w$])import\s*\(\s*['"](\.{1,2}\/[^'"]+)['"]\s*\)/g;
|
||||
const DYNAMIC=/(?<![.\w$])import\s*\(/g;
|
||||
// `import` as a word that is not a property name. The one rule for both patterns below: a word or $
|
||||
// before it, or a single dot (x.import, x?.import), makes it a property call; a dot that follows
|
||||
// another dot is the end of a spread (...import), which evaluates the call, so it is not excluded.
|
||||
const IMPORT_WORD=/(?<![\w$])(?<!(?<!\.)\.)import/.source;
|
||||
const DYNAMIC=new RegExp(`${IMPORT_WORD}\\s*\\(`,'g');
|
||||
const IMPORT=new RegExp([
|
||||
/(?:import|export)\s[^'"`;]*?\bfrom\s*['"](\.{1,2}\/[^'"]+)['"]/.source,
|
||||
/\bimport\s*['"](\.{1,2}\/[^'"]+)['"]/.source,
|
||||
`${DYNAMIC.source}\\s*['"](\\.{1,2}\\/[^'"]+)['"]\\s*\\)`,
|
||||
].join('|'),'g');
|
||||
const DYNAMIC_LITERAL=/^import\s*\(\s*(['"])[^'"\n]*\1\s*\)/;
|
||||
const REGEX_AFTER='(,=:[!&|?{};+-*%<>~^';
|
||||
const inert=(literals,index)=>literals.some(([from,to])=>index>=from&&index<to);
|
||||
|
||||
@@ -200,6 +200,59 @@ test('#779 R2 a substitution or the template text after it that never ends throw
|
||||
}
|
||||
});
|
||||
|
||||
// Each case is its own test, so one failing case does not hide the next.
|
||||
for(const [what,make] of [
|
||||
['an existing target',()=>withBusiness("const v=`${({...import('../../other/src/real.mjs')})}`;\n",{[`${EXT}real.mjs`]:EMPTY})],
|
||||
['a missing target',()=>withBusiness("const v=`${({...import('../../other/src/real.mjs')})}`;\n")],
|
||||
['a target outside the repository',()=>tree({[`${BIZ}a.mjs`]:"const v=`${({...import('../../../../outside.mjs')})}`;\n",'outside.mjs':EMPTY})],
|
||||
['an array spread of an existing target',()=>withBusiness("const v=`${[...import('../../other/src/real.mjs')]}`;\n",{[`${EXT}real.mjs`]:EMPTY})],
|
||||
]){
|
||||
test(`#779 R3 a spread before import inside a template substitution throws: ${what}`,()=>{
|
||||
assert.throws(()=>businessImportedFiles(make()),SUBSTITUTION);
|
||||
});
|
||||
}
|
||||
|
||||
for(const [what,source,files] of [
|
||||
['object spread',"const o={...import('../../other/src/real.mjs')};",{[`${EXT}real.mjs`]:EMPTY}],
|
||||
['array spread',"const l=[...import('../../other/src/real.mjs')];",{[`${EXT}real.mjs`]:EMPTY}],
|
||||
]){
|
||||
test(`#779 R3 a spread before import in plain code is a dynamic import and is found when its argument is a plain string: ${what}`,()=>{
|
||||
assert.deepEqual(businessImportedFiles(withBusiness(`${source}\n`,files)),['packages/other/src/real.mjs']);
|
||||
});
|
||||
}
|
||||
|
||||
test('#779 R3 a spread before import in plain code reaches the target checks: a missing target throws', ()=>{
|
||||
const missing=withBusiness("const o={...import('./missing.mjs')};\n");
|
||||
assert.throws(()=>businessImportedFiles(missing),/packages\/business\/src\/a\.mjs imports \.\/missing\.mjs, which does not exist/);
|
||||
});
|
||||
|
||||
for(const source of ['const o={...import(name)};','const l=[...import(name)];']){
|
||||
test(`#779 R3 a spread before import in plain code is refused when its argument is not a plain string: ${source}`,()=>{
|
||||
const repo=withBusiness(`${source}\n`);
|
||||
assert.throws(()=>businessImportedFiles(repo),/business\/src\/a\.mjs has a dynamic import that is not a plain string literal/);
|
||||
});
|
||||
}
|
||||
|
||||
test('#779 R3 a property call of import, x.import or x?.import, in plain code is not an import', ()=>{
|
||||
const repo=withBusiness([
|
||||
"loader.import('../../other/src/missing1.mjs');",
|
||||
"loader?.import('../../other/src/missing2.mjs');",
|
||||
'loader.import(name);',
|
||||
'loader?.import(name);',
|
||||
"import '../../other/src/real.mjs';",
|
||||
].join('\n'),{[`${EXT}real.mjs`]:EMPTY});
|
||||
assert.deepEqual(businessImportedFiles(repo),['packages/other/src/real.mjs']);
|
||||
});
|
||||
|
||||
test('#779 R3 a property call of import, x.import or x?.import, in a substitution is not an import and is not refused', ()=>{
|
||||
const repo=withBusiness([
|
||||
"const u=`${loader.import('../../other/src/missing3.mjs')}${loader?.import('../../other/src/missing4.mjs')}`;",
|
||||
'const w=`${loader.import(name)}${loader?.import(name)}`;',
|
||||
"import '../../other/src/real.mjs';",
|
||||
].join('\n'),{[`${EXT}real.mjs`]:EMPTY});
|
||||
assert.deepEqual(businessImportedFiles(repo),['packages/other/src/real.mjs']);
|
||||
});
|
||||
|
||||
test('#779 R2 a slash after a postfix ++ or -- is division, not a regular expression', ()=>{
|
||||
for(const op of ['++','--']){
|
||||
const repo=withBusiness([
|
||||
|
||||
Reference in new issue
Block a user