@malloydata/malloy 0.0.237-dev250225144145 → 0.0.237-dev250225213433

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.
@@ -2,10 +2,12 @@ import { Parser, Token } from 'antlr4ts';
2
2
  export interface ErrorCase {
3
3
  ruleContextOptions?: string[];
4
4
  offendingSymbol?: number;
5
+ offendingSymbolTextOptions?: string[];
5
6
  currentToken?: number;
6
7
  precedingTokenOptions?: number[][];
7
- lookAheadOptions?: number[][];
8
+ lookAheadOptions?: (number | string)[][];
8
9
  errorMessage: string;
9
- lookbackFromOffendingSymbol?: boolean;
10
+ lookbackSiblingRuleOptions?: number[];
11
+ predecessorHasAncestorRule?: number;
10
12
  }
11
13
  export declare const checkCustomErrorMessage: (parser: Parser, offendingSymbol: Token | undefined, errorCases: ErrorCase[]) => string;
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.checkCustomErrorMessage = void 0;
10
10
  const checkCustomErrorMessage = (parser, offendingSymbol, errorCases) => {
11
+ var _a, _b;
11
12
  const currentRuleName = parser.getRuleInvocationStack()[0];
12
13
  const currentToken = parser.currentToken;
13
14
  for (const errorCase of errorCases) {
@@ -17,52 +18,110 @@ const checkCustomErrorMessage = (parser, offendingSymbol, errorCases) => {
17
18
  (offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.type) === errorCase.offendingSymbol;
18
19
  const isRuleContextMatch = !errorCase.ruleContextOptions ||
19
20
  errorCase.ruleContextOptions.includes(currentRuleName);
20
- if (isCurrentTokenMatch && isOffendingSymbolMatch && isRuleContextMatch) {
21
+ const isOffendingSymbolTextMatch = !errorCase.offendingSymbolTextOptions ||
22
+ errorCase.offendingSymbolTextOptions.includes(((_a = offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.text) === null || _a === void 0 ? void 0 : _a.toLowerCase()) || '');
23
+ if (isCurrentTokenMatch &&
24
+ isOffendingSymbolMatch &&
25
+ isRuleContextMatch &&
26
+ isOffendingSymbolTextMatch) {
27
+ if (errorCase.lookbackSiblingRuleOptions) {
28
+ const siblings = parser.ruleContext.children;
29
+ if (!siblings) {
30
+ continue;
31
+ }
32
+ // We use 'any' here because the sibling isn't guaranteed to be a RuleContext,
33
+ // but we only care if it has a ruleIndex. If it doesn't then .includes() won't
34
+ // match anyways.
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const precedingRule = siblings[siblings.length - 1];
37
+ if (!errorCase.lookbackSiblingRuleOptions.includes(precedingRule.ruleIndex)) {
38
+ continue;
39
+ }
40
+ }
21
41
  // If so, try to check the preceding tokens.
22
42
  if (errorCase.precedingTokenOptions) {
23
- const hasPrecedingTokenMatch = errorCase.precedingTokenOptions.some(sequence => checkTokenSequenceMatch(parser, sequence, 'lookback', errorCase.lookbackFromOffendingSymbol
24
- ? offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.tokenIndex
25
- : undefined));
43
+ const hasPrecedingTokenMatch = errorCase.precedingTokenOptions.some(sequence => checkTokenSequenceMatch(parser, sequence, 'lookback', offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.tokenIndex));
26
44
  if (!hasPrecedingTokenMatch) {
27
45
  continue; // Continue to check a different error case
28
46
  }
29
47
  }
30
48
  if (errorCase.lookAheadOptions) {
31
- const hasLookaheadTokenMatch = errorCase.lookAheadOptions.some(sequence => checkTokenSequenceMatch(parser, sequence, 'lookahead', errorCase.lookbackFromOffendingSymbol
32
- ? offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.tokenIndex
33
- : undefined));
49
+ const hasLookaheadTokenMatch = errorCase.lookAheadOptions.some(sequence => checkTokenSequenceMatch(parser, sequence, 'lookahead', offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.tokenIndex));
34
50
  if (!hasLookaheadTokenMatch) {
35
51
  continue; // Continue to check a different error case
36
52
  }
37
53
  }
54
+ if (errorCase.predecessorHasAncestorRule) {
55
+ const precedingSibling = (_b = parser.ruleContext.children) === null || _b === void 0 ? void 0 : _b[0];
56
+ if (!precedingSibling ||
57
+ !doesRightmostBranchContainRule(precedingSibling, errorCase.predecessorHasAncestorRule)) {
58
+ continue;
59
+ }
60
+ }
38
61
  // If all cases match, return the custom error message
39
- const message = errorCase.errorMessage.replace('${currentToken}', (offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.text) || currentToken.text || '');
62
+ let message = errorCase.errorMessage
63
+ .replace('${currentToken}', currentToken.text || '')
64
+ .replace('${offendingSymbol}', (offendingSymbol === null || offendingSymbol === void 0 ? void 0 : offendingSymbol.text) || '');
65
+ try {
66
+ const previousToken = parser.inputStream.LT(-1);
67
+ message = message.replace('${previousToken}', previousToken.text || '');
68
+ }
69
+ catch (ex) {
70
+ // This shouldn't ever occur, but if it does, just leave the untokenized message.
71
+ }
40
72
  return message;
41
73
  }
42
74
  }
43
75
  return '';
44
76
  };
45
77
  exports.checkCustomErrorMessage = checkCustomErrorMessage;
78
+ // Recursively walk the rightmost branch (the path to the most recent token)
79
+ // to see whether any of those parse tree nodes match the provided rule number.
80
+ const doesRightmostBranchContainRule = (root, ruleNumber, depthLimit = 20) => {
81
+ var _a;
82
+ if (root.ruleIndex === ruleNumber) {
83
+ return true;
84
+ }
85
+ if (depthLimit <= 0) {
86
+ return false;
87
+ }
88
+ const childCount = ((_a = root.children) === null || _a === void 0 ? void 0 : _a.length) || 0;
89
+ if (root.children && childCount > 0) {
90
+ const rightmostChild = root.children[root.children.length - 1];
91
+ return doesRightmostBranchContainRule(rightmostChild, ruleNumber, depthLimit - 1);
92
+ }
93
+ return false;
94
+ };
46
95
  const checkTokenSequenceMatch = (parser, sequence, direction, anchorTokenPosition) => {
96
+ var _a;
47
97
  try {
48
98
  for (let i = 0; i < sequence.length; i++) {
49
99
  let streamToken = undefined;
50
100
  if (typeof anchorTokenPosition === 'number') {
51
101
  const tokenOffset = direction === 'lookahead' ? i + 1 : -1 * (i + 1);
52
- streamToken = parser.inputStream.get(anchorTokenPosition + tokenOffset).type;
102
+ streamToken = parser.inputStream.get(anchorTokenPosition + tokenOffset);
53
103
  }
54
104
  else {
55
105
  // Note: positive lookahead starts at '2' because '1' is the current token.
56
106
  const tokenOffset = direction === 'lookahead' ? i + 2 : -1 * (i + 1);
57
- streamToken = parser.inputStream.LA(tokenOffset);
107
+ streamToken = parser.inputStream.LT(tokenOffset);
58
108
  }
59
- // Note: negative checking is < -1 becuase Token.EOF is -1, but below
60
- // that we use negatives to indicate "does-not-match" rules.
61
- if (sequence[i] >= -1 && streamToken !== sequence[i]) {
62
- return false;
109
+ if (typeof sequence[i] === 'number') {
110
+ const tokenIndex = sequence[i];
111
+ // Note: negative checking is < -1 becuase Token.EOF is -1, but below
112
+ // that we use negatives to indicate "does-not-match" rules.
113
+ if (tokenIndex >= -1 && streamToken.type !== tokenIndex) {
114
+ return false;
115
+ }
116
+ if (tokenIndex < -1 && streamToken.type === -1 * tokenIndex) {
117
+ return false;
118
+ }
63
119
  }
64
- if (sequence[i] < -1 && streamToken === -1 * sequence[i]) {
65
- return false;
120
+ else if (typeof sequence[i] === 'string') {
121
+ const tokenText = sequence[i];
122
+ if (tokenText !== ((_a = streamToken.text) === null || _a === void 0 ? void 0 : _a.toLowerCase())) {
123
+ return false;
124
+ }
66
125
  }
67
126
  }
68
127
  return true;
@@ -1,14 +1,12 @@
1
- import { DefaultErrorStrategy, Parser } from 'antlr4ts';
1
+ import { DefaultErrorStrategy } from 'antlr4ts';
2
2
  /**
3
- * Custom error strategy for the Malloy Parser. This strategy attempts to
4
- * detect known cases where the default error strategy results in an unhelpful
5
- * parse tree or misleading messages. In any cases not explicitly handled, this
6
- * custom error strategy will fall back to the default error strategy.
3
+ * Custom error strategy for the Malloy Parser.
4
+ *
5
+ * This class does not currently override default ANTLR error handling.
7
6
  *
8
7
  * For more details, read the documentation in DefaultErrorStrategy.d.ts
9
8
  * or reference the superclass at:
10
9
  * https://github.com/tunnelvisionlabs/antlr4ts/blob/master/src/DefaultErrorStrategy.ts
11
10
  */
12
11
  export declare class MalloyErrorStrategy extends DefaultErrorStrategy {
13
- sync(parser: Parser): void;
14
12
  }
@@ -8,35 +8,16 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.MalloyErrorStrategy = void 0;
10
10
  const antlr4ts_1 = require("antlr4ts");
11
- const MalloyParser_1 = require("../lib/Malloy/MalloyParser");
12
- const custom_error_messages_1 = require("./custom-error-messages");
13
- const customErrorCases = [
14
- {
15
- errorMessage: "Missing '{' after 'extend'",
16
- currentToken: MalloyParser_1.MalloyParser.EXTEND,
17
- ruleContextOptions: ['sqExpr'],
18
- lookAheadOptions: [[-MalloyParser_1.MalloyParser.OCURLY]],
19
- },
20
- ];
21
11
  /**
22
- * Custom error strategy for the Malloy Parser. This strategy attempts to
23
- * detect known cases where the default error strategy results in an unhelpful
24
- * parse tree or misleading messages. In any cases not explicitly handled, this
25
- * custom error strategy will fall back to the default error strategy.
12
+ * Custom error strategy for the Malloy Parser.
13
+ *
14
+ * This class does not currently override default ANTLR error handling.
26
15
  *
27
16
  * For more details, read the documentation in DefaultErrorStrategy.d.ts
28
17
  * or reference the superclass at:
29
18
  * https://github.com/tunnelvisionlabs/antlr4ts/blob/master/src/DefaultErrorStrategy.ts
30
19
  */
31
20
  class MalloyErrorStrategy extends antlr4ts_1.DefaultErrorStrategy {
32
- sync(parser) {
33
- const interceptedErrorMessage = (0, custom_error_messages_1.checkCustomErrorMessage)(parser, undefined, customErrorCases);
34
- if (interceptedErrorMessage) {
35
- parser.notifyErrorListeners(interceptedErrorMessage, parser.currentToken, undefined);
36
- return;
37
- }
38
- super.sync(parser);
39
- }
40
21
  }
41
22
  exports.MalloyErrorStrategy = MalloyErrorStrategy;
42
23
  //# sourceMappingURL=malloy-error-strategy.js.map
@@ -2,7 +2,7 @@ import { ANTLRErrorListener, Token } from 'antlr4ts';
2
2
  import { ErrorCase } from './custom-error-messages';
3
3
  import { MessageLogger, MessageCode, MessageParameterType, LogMessageOptions } from '../parse-log';
4
4
  import { MalloyTranslation } from '../parse-malloy';
5
- export declare const commonErrorCases: ErrorCase[];
5
+ export declare const malloyCustomErrorCases: ErrorCase[];
6
6
  export declare class MalloyParserErrorListener implements ANTLRErrorListener<Token> {
7
7
  readonly translator: MalloyTranslation;
8
8
  readonly messages: MessageLogger;
@@ -6,11 +6,19 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.MalloyParserErrorListener = exports.commonErrorCases = void 0;
9
+ exports.MalloyParserErrorListener = exports.malloyCustomErrorCases = void 0;
10
10
  const MalloyParser_1 = require("../lib/Malloy/MalloyParser");
11
11
  const custom_error_messages_1 = require("./custom-error-messages");
12
12
  const parse_log_1 = require("../parse-log");
13
- exports.commonErrorCases = [
13
+ // A set of custom error messages and their triggering cases,
14
+ // used for syntax error message re-writing when ANTLR would
15
+ // otherwise print a standard (and not very useful) error message.
16
+ //
17
+ // Most Malloy errors are detected and generated in language elements
18
+ // inside `src/lang/ast`. These custom error messages are used to
19
+ // cover a variety of cases that are not as straightforward to
20
+ // catch in the AST.
21
+ exports.malloyCustomErrorCases = [
14
22
  {
15
23
  errorMessage: "'view:' must be followed by '<identifier> is {'",
16
24
  ruleContextOptions: ['exploreQueryDef'],
@@ -18,7 +26,7 @@ exports.commonErrorCases = [
18
26
  precedingTokenOptions: [[MalloyParser_1.MalloyParser.VIEW], [MalloyParser_1.MalloyParser.COLON]],
19
27
  },
20
28
  {
21
- errorMessage: "Missing '}' at '${currentToken}'",
29
+ errorMessage: "Missing '}' at '${offendingSymbol}'",
22
30
  ruleContextOptions: ['vExpr'],
23
31
  offendingSymbol: MalloyParser_1.MalloyParser.VIEW,
24
32
  currentToken: MalloyParser_1.MalloyParser.OCURLY,
@@ -30,17 +38,106 @@ exports.commonErrorCases = [
30
38
  'queryProperties',
31
39
  'exploreStatement',
32
40
  ],
33
- lookAheadOptions: [
34
- [MalloyParser_1.MalloyParser.EOF],
35
- [MalloyParser_1.MalloyParser.RUN],
36
- [MalloyParser_1.MalloyParser.SOURCE],
37
- ],
41
+ offendingSymbolTextOptions: ['<eof>', 'run:', 'source:'],
38
42
  },
39
43
  {
40
44
  errorMessage: "'aggregate:' entries must include a name (ex: `some_name is count()`)",
41
45
  precedingTokenOptions: [[MalloyParser_1.MalloyParser.AGGREGATE]],
42
46
  lookAheadOptions: [[-MalloyParser_1.MalloyParser.IS]],
43
- lookbackFromOffendingSymbol: true,
47
+ },
48
+ {
49
+ errorMessage: "Expected ':' following 'source'",
50
+ offendingSymbol: MalloyParser_1.MalloyParser.SOURCE_KW,
51
+ ruleContextOptions: ['malloyDocument'],
52
+ },
53
+ {
54
+ errorMessage: "Expected ':' following '${offendingSymbol}'",
55
+ offendingSymbolTextOptions: [
56
+ 'dimension',
57
+ 'measure',
58
+ 'where',
59
+ 'declare',
60
+ 'join_one',
61
+ 'join_many',
62
+ 'join_cross',
63
+ 'primary_key',
64
+ ],
65
+ ruleContextOptions: ['exploreStatement'],
66
+ },
67
+ {
68
+ errorMessage: "Expected 'is' or '(' following identifier '${previousToken}'",
69
+ ruleContextOptions: ['sourceDefinition'],
70
+ lookbackSiblingRuleOptions: [
71
+ MalloyParser_1.MalloyParser.RULE_sourceNameDef,
72
+ MalloyParser_1.MalloyParser.RULE_sourceParameters,
73
+ ],
74
+ },
75
+ {
76
+ errorMessage: "Unexpected '{' following source expression. Expected: 'extend', 'include', '+' or '->'",
77
+ offendingSymbol: MalloyParser_1.MalloyParser.OCURLY,
78
+ ruleContextOptions: ['malloyDocument'],
79
+ predecessorHasAncestorRule: MalloyParser_1.MalloyParser.RULE_sqExplore,
80
+ },
81
+ {
82
+ errorMessage: "Unexpected 'join:'. Did you mean 'join_one:', 'join_many:' or 'join_cross:'?",
83
+ ruleContextOptions: ['exploreStatement'],
84
+ offendingSymbolTextOptions: ['join'],
85
+ lookAheadOptions: [[MalloyParser_1.MalloyParser.COLON]],
86
+ },
87
+ {
88
+ errorMessage: "Unexpected '${offendingSymbol}'. Did you mean 'primary_key:'?",
89
+ ruleContextOptions: ['exploreStatement'],
90
+ offendingSymbolTextOptions: ['primarykey', 'primary'],
91
+ lookAheadOptions: [
92
+ [MalloyParser_1.MalloyParser.COLON],
93
+ ['key', MalloyParser_1.MalloyParser.COLON],
94
+ ['key', MalloyParser_1.MalloyParser.IDENTIFIER],
95
+ ],
96
+ },
97
+ {
98
+ errorMessage: "Unexpected '${offendingSymbol}'. Did you mean 'group_by:'?",
99
+ ruleContextOptions: ['queryStatement'],
100
+ offendingSymbolTextOptions: ['groupby', 'group'],
101
+ lookAheadOptions: [
102
+ [MalloyParser_1.MalloyParser.COLON],
103
+ ['by', MalloyParser_1.MalloyParser.COLON],
104
+ ['by', MalloyParser_1.MalloyParser.IDENTIFIER],
105
+ ],
106
+ },
107
+ {
108
+ errorMessage: "Unexpected '${offendingSymbol}'. Did you mean 'order_by:'?",
109
+ ruleContextOptions: ['queryStatement'],
110
+ offendingSymbolTextOptions: ['orderby', 'order'],
111
+ lookAheadOptions: [
112
+ [MalloyParser_1.MalloyParser.COLON],
113
+ ['by', MalloyParser_1.MalloyParser.COLON],
114
+ ['by', MalloyParser_1.MalloyParser.IDENTIFIER],
115
+ ],
116
+ },
117
+ {
118
+ errorMessage: "Expected ':' following '${offendingSymbol}'",
119
+ offendingSymbolTextOptions: [
120
+ 'group_by',
121
+ 'declare',
122
+ 'join_one',
123
+ 'join_many',
124
+ 'join_cross',
125
+ 'extend',
126
+ 'select',
127
+ 'project',
128
+ 'index',
129
+ 'aggregate',
130
+ 'calculate',
131
+ 'top',
132
+ 'limit',
133
+ 'order_by',
134
+ 'where',
135
+ 'having',
136
+ 'nest',
137
+ 'sample',
138
+ 'timezone',
139
+ ],
140
+ ruleContextOptions: ['queryStatement'],
44
141
  },
45
142
  ];
46
143
  class MalloyParserErrorListener {
@@ -57,7 +154,7 @@ class MalloyParserErrorListener {
57
154
  const range = offendingSymbol
58
155
  ? this.translator.rangeFromToken(offendingSymbol)
59
156
  : { start: errAt, end: errAt };
60
- const overrideMessage = (0, custom_error_messages_1.checkCustomErrorMessage)(recognizer, offendingSymbol, exports.commonErrorCases);
157
+ const overrideMessage = (0, custom_error_messages_1.checkCustomErrorMessage)(recognizer, offendingSymbol, exports.malloyCustomErrorCases);
61
158
  if (overrideMessage) {
62
159
  message = overrideMessage;
63
160
  }
@@ -8,62 +8,91 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  const test_translator_1 = require("./test-translator");
10
10
  require("./parse-expects");
11
- describe('errors', () => {
12
- test('source missing closing curly: EOF', () => {
13
- expect('source: x is a extend {').toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at '<EOF>'"));
11
+ /**
12
+ * Unit tests intended to cover the custom error messages defined in
13
+ * malloy-custom-error-messages.ts.
14
+ *
15
+ * These tests are intended to prevent unintended regressions. It is
16
+ * okay to change or remove tests here as long as you are doing so
17
+ * for a good reason.
18
+ */
19
+ describe('custom error messages', () => {
20
+ describe('source', () => {
21
+ test('missing alias', () => {
22
+ expect('source: x extend { }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected 'is' or '(' following identifier 'x'"));
23
+ });
24
+ test('missing closing curly: EOF', () => {
25
+ expect('source: x is a extend {').toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at '<EOF>'"));
26
+ });
27
+ test('source: missing colon in root context', () => {
28
+ expect('source x is a extend { }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'source'"));
29
+ });
30
+ test('opening curly to EOF', () => {
31
+ expect(`
32
+ source: y is x extend {
33
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at '<EOF>'"));
34
+ });
35
+ test('expression missing operand before curly', () => {
36
+ expect("source: a is presto.table('malloytest.state_facts') {}").toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected '{' following source expression. Expected: 'extend', 'include', '+' or '->'"));
37
+ });
38
+ test('missing opening curly after source extend keyword', () => {
39
+ expect(`
40
+ source: x is a extend
41
+ primary_key: id
42
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("missing '{' at 'primary_key:'"));
43
+ });
14
44
  });
15
- test('view is missing name', () => {
16
- expect(`
17
- source: x is a extend {
18
- view: {
19
- group_by: b
45
+ describe('exploreProperties', () => {
46
+ test('misspelled "join"', () => {
47
+ expect(`source: x is a extend {
48
+ join: data is y on dataId = data.id
49
+ }`).toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'join:'. Did you mean 'join_one:', 'join_many:' or 'join_cross:'?"));
50
+ expect(`source: x is a extend {
51
+ primary_key: name
52
+ join: data is y on dataId = data.id
53
+ }`).toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'join:'. Did you mean 'join_one:', 'join_many:' or 'join_cross:'?"));
54
+ });
55
+ test('misspelled "primary_key:"', () => {
56
+ expect('source: x is a extend { primaryKey: name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'primaryKey'. Did you mean 'primary_key:'?"));
57
+ expect('source: x is a extend { primary key: name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'primary'. Did you mean 'primary_key:'?"));
58
+ });
59
+ test('explore statement keyword missing colon', () => {
60
+ expect('source: x is a extend { where x > 5 }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'where'"));
61
+ expect('source: x is a extend { primary_key name }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'primary_key'"));
62
+ expect('source: x is a extend { declare total is value.sum() }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'declare'"));
63
+ });
64
+ test('incorrect opening curly after dimension', () => {
65
+ expect(`
66
+ source: x is a extend {
67
+ dimension: {
68
+ test is best
69
+ }
20
70
  }
21
- }
22
- `).toLogAtLeast((0, test_translator_1.errorMessage)("'view:' must be followed by '<identifier> is {'"));
71
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("extraneous input '{' expecting {BQ_STRING, IDENTIFIER}"));
72
+ });
23
73
  });
24
- test('missing closing curly, source>view', () => {
25
- expect(`
26
- source: x is a extend {
27
- view: y is {
28
- group_by: b
74
+ describe('view', () => {
75
+ test('view is missing name', () => {
76
+ expect(`
77
+ source: x is a extend { view: {
78
+ group_by: b
79
+ } }
80
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("'view:' must be followed by '<identifier> is {'"));
81
+ });
82
+ test('missing closing curly, source>view', () => {
83
+ expect(`
84
+ source: x is a extend {
85
+ view: y is {
86
+ group_by: b
29
87
 
30
- view: z is {
31
- group_by: c
32
- }
33
- }
34
- `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at 'view:'"));
35
- });
36
- test('incorrect opening curly after dimension', () => {
37
- expect(`
38
- source: x is a extend {
39
- dimension: {
40
- test is best
88
+ view: z is {
89
+ group_by: c
90
+ }
41
91
  }
42
- }
43
- `).toLogAtLeast((0, test_translator_1.errorMessage)("extraneous input '{' expecting {BQ_STRING, IDENTIFIER}"));
44
- });
45
- test('misspelled primarykey', () => {
46
- expect(`
47
- source: x is a extend {
48
- primarykey: id
49
- }
50
- `).toLogAtLeast((0, test_translator_1.errorMessage)("no viable alternative at input 'primarykey'"));
51
- });
52
- test('missing opening curly after source extend keyword', () => {
53
- expect(`
54
- source: x is a extend
55
- primary_key: id
56
- `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '{' after 'extend'"));
57
- });
58
- test('missing alias for aggregate entry', () => {
59
- expect(`
60
- run: x -> {
61
- aggregate: count()
62
- }
63
- `).toLogAtLeast((0, test_translator_1.errorMessage)("'aggregate:' entries must include a name (ex: `some_name is count()`)"));
64
- });
65
- test('missing alias for aggregate inside source>view', () => {
66
- expect(`
92
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at 'view:'"));
93
+ });
94
+ test('missing alias for aggregate inside source>view', () => {
95
+ expect(`
67
96
  source: x is aa extend {
68
97
  measure: airport_count is count()
69
98
 
@@ -73,16 +102,41 @@ describe('errors', () => {
73
102
  }
74
103
  }
75
104
  `).toLogAtLeast((0, test_translator_1.errorMessage)("'aggregate:' entries must include a name (ex: `some_name is count()`)"));
105
+ });
76
106
  });
77
- test('run opening curly to EOF', () => {
78
- expect(`
79
- run: x -> {
80
- `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at '<EOF>'"));
107
+ describe('queryStatement', () => {
108
+ test('misspelled group_by:', () => {
109
+ expect('source: x is a -> { groupBy: name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'groupBy'. Did you mean 'group_by:'?"));
110
+ expect('source: x is a -> { group by: name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'group'. Did you mean 'group_by:'?"));
111
+ expect('source: x is a -> { group by name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'group'. Did you mean 'group_by:'?"));
112
+ });
113
+ test('misspelled order_by:', () => {
114
+ expect('source: x is a -> { orderBy: name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'orderBy'. Did you mean 'order_by:'?"));
115
+ expect('source: x is a -> { order by: name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'order'. Did you mean 'order_by:'?"));
116
+ expect('source: x is a -> { order by name }').toLogAtLeast((0, test_translator_1.errorMessage)("Unexpected 'order'. Did you mean 'order_by:'?"));
117
+ });
118
+ test('query statement keyword missing colon', () => {
119
+ expect('source: x is a -> { group_by x > 5 }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'group_by'"));
120
+ expect('source: x is a -> { declare name is id }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'declare'"));
121
+ expect('source: x is a -> { select * }').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'select'"));
122
+ expect('source: x is a -> { project i, j, k}').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'project'"));
123
+ expect('source: x is a -> { where val > 5}').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'where'"));
124
+ expect('source: x is a -> { order_by name desc, 2 asc}').toLogAtLeast((0, test_translator_1.errorMessage)("Expected ':' following 'order_by'"));
125
+ });
81
126
  });
82
- test('source opening curly to EOF', () => {
83
- expect(`
84
- source: y is x extend {
85
- `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at '<EOF>'"));
127
+ describe('run', () => {
128
+ test('missing alias for aggregate entry', () => {
129
+ expect(`
130
+ run: x -> {
131
+ aggregate: count()
132
+ }
133
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("'aggregate:' entries must include a name (ex: `some_name is count()`)"));
134
+ });
135
+ test('run opening curly to EOF', () => {
136
+ expect(`
137
+ run: x -> {
138
+ `).toLogAtLeast((0, test_translator_1.errorMessage)("Missing '}' at '<EOF>'"));
139
+ });
86
140
  });
87
141
  });
88
142
  //# sourceMappingURL=syntax-errors.spec.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.237-dev250225144145",
3
+ "version": "0.0.237-dev250225213433",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -41,8 +41,8 @@
41
41
  "generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@malloydata/malloy-interfaces": "^0.0.237-dev250225144145",
45
- "@malloydata/malloy-tag": "^0.0.237-dev250225144145",
44
+ "@malloydata/malloy-interfaces": "^0.0.237-dev250225213433",
45
+ "@malloydata/malloy-tag": "^0.0.237-dev250225213433",
46
46
  "antlr4ts": "^0.5.0-alpha.4",
47
47
  "assert": "^2.0.0",
48
48
  "jest-diff": "^29.6.2",