@vltpkg/dss-parser 1.3.7 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,3 +14,29 @@ import { parse } from '@vltpkg/dss-parser'
14
14
  // Parse a selector string into an AST
15
15
  const ast = parse(':root > *')
16
16
  ```
17
+
18
+ ## Validation
19
+
20
+ `parse()` throws a `SyntaxError` on invalid DSS, with `cause.code`
21
+ `EQUERY` and the offending input in `cause.found`. Rejected:
22
+
23
+ - malformed syntax, e.g. `:outdated(`, `[name=`
24
+ - empty / whitespace-only queries and empty selectors (`#a,`,
25
+ `:not()`)
26
+ - unknown pseudo-classes, combinators and attribute operators
27
+ - dangling combinators, e.g. `:root >`, `> >`
28
+ - bare words, strings, empty ids / attributes, e.g. `foo`, `.dev`,
29
+ `"foo"`, `#`, `[]`
30
+
31
+ `:is()` arguments are a forgiving list: only the last group above is
32
+ rejected there.
33
+
34
+ Pass `{ loose: true }` to skip the DSS checks (malformed syntax still
35
+ throws):
36
+
37
+ ```js
38
+ parse('#a, :unknown', { loose: true }) // no throw
39
+ ```
40
+
41
+ Supported names are exported as `pseudoClassNames`, `combinatorNames`
42
+ and `attributeOperatorNames`.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pseudo-class names supported by DSS, without the leading `:`.
3
+ */
4
+ export declare const pseudoClassNames: readonly ["abandoned", "attr", "built", "confused", "cve", "cwe", "debug", "deprecated", "dev", "diff", "dist", "dynamic", "empty", "entropic", "env", "eval", "fs", "has", "host", "hostname", "is", "license", "link", "malware", "minified", "missing", "native", "network", "not", "obfuscated", "optional", "outdated", "overridden", "path", "peer", "prerelease", "private", "prod", "project", "published", "registry", "root", "scanned", "scope", "score", "scripts", "semver", "sev", "severity", "shell", "shrinkwrap", "spec", "squat", "suspicious", "tracker", "trivial", "type", "undesirable", "unknown", "unmaintained", "unpopular", "unstable", "v", "vuln", "vulnerable", "workspace"];
5
+ export type PseudoClassName = (typeof pseudoClassNames)[number];
6
+ /**
7
+ * Combinators supported by DSS.
8
+ */
9
+ export declare const combinatorNames: readonly [">", "~", " "];
10
+ export type CombinatorName = (typeof combinatorNames)[number];
11
+ /**
12
+ * Attribute selector operators supported by DSS.
13
+ */
14
+ export declare const attributeOperatorNames: readonly ["=", "^=", "$=", "~=", "*=", "|="];
15
+ export type AttributeOperatorName = (typeof attributeOperatorNames)[number];
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Pseudo-class names supported by DSS, without the leading `:`.
3
+ */
4
+ export const pseudoClassNames = [
5
+ 'abandoned',
6
+ 'attr',
7
+ 'built',
8
+ 'confused',
9
+ 'cve',
10
+ 'cwe',
11
+ 'debug',
12
+ 'deprecated',
13
+ 'dev',
14
+ 'diff',
15
+ 'dist',
16
+ 'dynamic',
17
+ 'empty',
18
+ 'entropic',
19
+ 'env',
20
+ 'eval',
21
+ 'fs',
22
+ 'has',
23
+ 'host',
24
+ 'hostname',
25
+ 'is',
26
+ 'license',
27
+ 'link',
28
+ 'malware',
29
+ 'minified',
30
+ 'missing',
31
+ 'native',
32
+ 'network',
33
+ 'not',
34
+ 'obfuscated',
35
+ 'optional',
36
+ 'outdated',
37
+ 'overridden',
38
+ 'path',
39
+ 'peer',
40
+ 'prerelease',
41
+ 'private',
42
+ 'prod',
43
+ 'project',
44
+ 'published',
45
+ 'registry',
46
+ 'root',
47
+ 'scanned',
48
+ 'scope',
49
+ 'score',
50
+ 'scripts',
51
+ 'semver',
52
+ 'sev',
53
+ 'severity',
54
+ 'shell',
55
+ 'shrinkwrap',
56
+ 'spec',
57
+ 'squat',
58
+ 'suspicious',
59
+ 'tracker',
60
+ 'trivial',
61
+ 'type',
62
+ 'undesirable',
63
+ 'unknown',
64
+ 'unmaintained',
65
+ 'unpopular',
66
+ 'unstable',
67
+ 'v',
68
+ 'vuln',
69
+ 'vulnerable',
70
+ 'workspace',
71
+ ];
72
+ /**
73
+ * Combinators supported by DSS.
74
+ */
75
+ export const combinatorNames = ['>', '~', ' '];
76
+ /**
77
+ * Attribute selector operators supported by DSS.
78
+ */
79
+ export const attributeOperatorNames = [
80
+ '=',
81
+ '^=',
82
+ '$=',
83
+ '~=',
84
+ '*=',
85
+ '|=',
86
+ ];
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Root } from 'postcss-selector-parser';
2
2
  export * from './types.ts';
3
+ export * from './grammar.ts';
3
4
  /**
4
5
  * Escapes forward slashes in specific patterns matching @scoped/name paths
5
6
  * This will allow usage of unescaped forward slashes necessary for scoped
@@ -8,8 +9,13 @@ export * from './types.ts';
8
9
  export declare const escapeScopedNamesSlashes: (query: string) => string;
9
10
  export declare const escapeDots: (query: string) => string;
10
11
  export declare const unescapeDots: (query: string) => string;
12
+ export type ParseOptions = {
13
+ /** skip DSS grammar checks, malformed syntax still throws */
14
+ loose?: boolean;
15
+ };
11
16
  /**
12
- * Parses a CSS selector string into an AST
17
+ * Parses a DSS query string into an AST
13
18
  * Handles escaping of forward slashes in specific patterns
19
+ * Throws an `EQUERY` SyntaxError on invalid DSS
14
20
  */
15
- export declare const parse: (query: string) => Root;
21
+ export declare const parse: (query: string, { loose }?: ParseOptions) => Root;
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import postcssSelectorParser from 'postcss-selector-parser';
2
- import { asSelectorNode, isCombinatorNode, isPseudoNode, isTagNode, } from "./types.js";
2
+ import { syntaxError } from '@vltpkg/error-cause';
3
+ import { attributeOperatorNames, combinatorNames, pseudoClassNames, } from "./grammar.js";
4
+ import { asSelectorNode, isAttributeNode, isCombinatorNode, isPseudoNode, isStringNode, isTagNode, } from "./types.js";
3
5
  export * from "./types.js";
6
+ export * from "./grammar.js";
4
7
  /**
5
8
  * Escapes forward slashes in specific patterns matching @scoped/name paths
6
9
  * This will allow usage of unescaped forward slashes necessary for scoped
@@ -21,11 +24,78 @@ const pseudoCleanUpNeeded = new Set([
21
24
  ':vulnerable',
22
25
  ]);
23
26
  const hasParamsToEscape = (node) => pseudoCleanUpNeeded.has(node.value);
27
+ const pseudoClasses = new Set(pseudoClassNames);
28
+ const combinators = new Set(combinatorNames);
29
+ const attributeOperators = new Set(attributeOperatorNames);
30
+ // pseudo-classes taking selector args, all others take strings
31
+ const selectorPseudos = new Set(['has', 'is', 'not']);
32
+ const invalid = (message, found) => syntaxError(message, { code: 'EQUERY', found });
24
33
  /**
25
- * Parses a CSS selector string into an AST
34
+ * Checks a parsed selector against the DSS grammar.
35
+ * `forgiving`: inside `:is()`, only bare words / strings / empty
36
+ * `#` `[]` are rejected.
37
+ */
38
+ const validate = (selector, query, forgiving = false) => {
39
+ const nodes = selector.nodes.filter(n => n.type !== 'comment');
40
+ if (!nodes.length && !forgiving) {
41
+ throw invalid('Empty selector', query);
42
+ }
43
+ nodes.forEach((node, i) => {
44
+ // `#` / `[]`: nothing to match
45
+ if ((node.type === 'id' && !node.value) ||
46
+ (node.type === 'attribute' && !node.attribute)) {
47
+ throw invalid('Unsupported selector', node.type === 'id' ? '#' : '[]');
48
+ }
49
+ if (isCombinatorNode(node)) {
50
+ if (forgiving)
51
+ return;
52
+ if (!combinators.has(node.value)) {
53
+ throw syntaxError(`Unsupported combinator: ${node.value}`, {
54
+ code: 'EQUERY',
55
+ found: node.value,
56
+ validOptions: combinatorNames,
57
+ });
58
+ }
59
+ const next = nodes[i + 1];
60
+ if (!next || isCombinatorNode(next)) {
61
+ throw invalid('Dangling combinator', query);
62
+ }
63
+ }
64
+ else if (isPseudoNode(node)) {
65
+ const name = node.value.slice(1);
66
+ if (!pseudoClasses.has(name)) {
67
+ if (forgiving)
68
+ return;
69
+ throw invalid(`Unsupported pseudo-class: ${node.value}`, node.value);
70
+ }
71
+ if (selectorPseudos.has(name)) {
72
+ for (const n of node.nodes) {
73
+ validate(asSelectorNode(n), query, forgiving || name === 'is');
74
+ }
75
+ }
76
+ }
77
+ else if (isAttributeNode(node)) {
78
+ if (!forgiving &&
79
+ node.operator &&
80
+ !attributeOperators.has(node.operator)) {
81
+ throw invalid(`Unsupported attribute operator: ${node.operator}`, node.operator);
82
+ }
83
+ }
84
+ else if ((isTagNode(node) && node.value !== '{' && node.value !== '}') ||
85
+ isStringNode(node)) {
86
+ // `{` / `}` tags are a noop in the query engine
87
+ throw invalid('Unsupported selector', node.value);
88
+ }
89
+ });
90
+ };
91
+ /**
92
+ * Parses a DSS query string into an AST
26
93
  * Handles escaping of forward slashes in specific patterns
94
+ * Throws an `EQUERY` SyntaxError on invalid DSS
27
95
  */
28
- export const parse = (query) => {
96
+ export const parse = (query, { loose = false } = {}) => {
97
+ if (!loose && !query.trim())
98
+ throw invalid('Empty query', query);
29
99
  const escapedQuery = escapeDots(escapeScopedNamesSlashes(query));
30
100
  const transformAst = (root) => {
31
101
  root.walk((node) => {
@@ -41,6 +111,8 @@ export const parse = (query) => {
41
111
  for (const n of node.nodes) {
42
112
  // the parameters have a selector node that wraps them up
43
113
  const selector = asSelectorNode(n);
114
+ if (!selector.nodes.length)
115
+ continue;
44
116
  selector.nodes.forEach((currentNode, index, arr) => {
45
117
  // get the next node, we'll update it later
46
118
  const nextNode = arr[index + 1];
@@ -89,5 +161,28 @@ export const parse = (query) => {
89
161
  }
90
162
  });
91
163
  };
92
- return postcssSelectorParser(transformAst).astSync(escapedQuery);
164
+ let ast;
165
+ try {
166
+ ast = postcssSelectorParser(transformAst).astSync(escapedQuery);
167
+ }
168
+ catch (err) {
169
+ // postcss crashes with a TypeError when input ends after `(`
170
+ const reason = err instanceof TypeError ?
171
+ 'unexpected end of input'
172
+ : err.message;
173
+ throw syntaxError(`Invalid query syntax: ${reason}`, {
174
+ code: 'EQUERY',
175
+ found: query,
176
+ cause: err,
177
+ });
178
+ }
179
+ if (!loose) {
180
+ // postcss silently drops a trailing top-level comma
181
+ if (/(^|[^\\])(\\\\)*,\s*$/.test(query)) {
182
+ throw invalid('Empty selector', query);
183
+ }
184
+ for (const s of ast.nodes)
185
+ validate(s, query);
186
+ }
187
+ return ast;
93
188
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vltpkg/dss-parser",
3
3
  "description": "The Dependency Selector Syntax (DSS) parser",
4
- "version": "1.3.7",
4
+ "version": "1.4.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/vltpkg/vltpkg.git",
@@ -13,7 +13,7 @@
13
13
  "url": "http://vlt.sh"
14
14
  },
15
15
  "dependencies": {
16
- "@vltpkg/error-cause": "1.3.7",
16
+ "@vltpkg/error-cause": "1.4.0",
17
17
  "postcss-selector-parser": "^7.1.1"
18
18
  },
19
19
  "devDependencies": {