Skip to content

Commit 76843be

Browse files
authored
[BUGFIX][needs-docs] Allow expression parser to report better error location
We return the line and column to allow builder to highlight that location for the user.
1 parent b6242b4 commit 76843be

File tree

9 files changed

+214
-17
lines changed

9 files changed

+214
-17
lines changed

python/core/expression/qgsexpression.sip.in

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,28 @@ Implicit sharing was added in 2.14
6666
%End
6767
public:
6868

69+
struct ParserError
70+
{
71+
enum ParserErrorType
72+
{
73+
Unknown,
74+
FunctionUnknown,
75+
FunctionWrongArgs,
76+
FunctionInvalidParams,
77+
FunctionNamedArgsError
78+
};
79+
80+
ParserErrorType errorType;
81+
82+
int firstLine;
83+
84+
int firstColumn;
85+
86+
int lastLine;
87+
88+
int lastColumn;
89+
};
90+
6991
QgsExpression( const QString &expr );
7092
%Docstring
7193
Creates a new expression based on the provided string.
@@ -109,6 +131,13 @@ Returns true if an error occurred when parsing the input expression
109131
QString parserErrorString() const;
110132
%Docstring
111133
Returns parser error
134+
%End
135+
136+
ParserError parserError() const;
137+
%Docstring
138+
Returns parser error details including location of error.
139+
140+
.. versionadded:: 3.0
112141
%End
113142

114143
const QgsExpressionNode *rootNode() const;

src/core/expression/qgsexpression.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727

2828
// from parser
29-
extern QgsExpressionNode *parseExpression( const QString &str, QString &parserErrorMsg );
29+
extern QgsExpressionNode *parseExpression( const QString &str, QString &parserErrorMsg, QgsExpression::ParserError &parserError );
3030

3131
///////////////////////////////////////////////
3232
// QVariant checks and conversions
@@ -99,7 +99,7 @@ bool QgsExpression::checkExpression( const QString &text, const QgsExpressionCon
9999
void QgsExpression::setExpression( const QString &expression )
100100
{
101101
detach();
102-
d->mRootNode = ::parseExpression( expression, d->mParserErrorString );
102+
d->mRootNode = ::parseExpression( expression, d->mParserErrorString, d->mParserError );
103103
d->mEvalErrorString = QString();
104104
d->mExp = expression;
105105
}
@@ -195,7 +195,7 @@ int QgsExpression::functionCount()
195195
QgsExpression::QgsExpression( const QString &expr )
196196
: d( new QgsExpressionPrivate )
197197
{
198-
d->mRootNode = ::parseExpression( expr, d->mParserErrorString );
198+
d->mRootNode = ::parseExpression( expr, d->mParserErrorString, d->mParserError );
199199
d->mExp = expr;
200200
Q_ASSERT( !d->mParserErrorString.isNull() || d->mRootNode );
201201
}
@@ -255,6 +255,11 @@ QString QgsExpression::parserErrorString() const
255255
return d->mParserErrorString;
256256
}
257257

258+
QgsExpression::ParserError QgsExpression::parserError() const
259+
{
260+
return d->mParserError;
261+
}
262+
258263
QSet<QString> QgsExpression::referencedColumns() const
259264
{
260265
if ( !d->mRootNode )
@@ -338,7 +343,7 @@ bool QgsExpression::prepare( const QgsExpressionContext *context )
338343
//re-parse expression. Creation of QgsExpressionContexts may have added extra
339344
//known functions since this expression was created, so we have another try
340345
//at re-parsing it now that the context must have been created
341-
d->mRootNode = ::parseExpression( d->mExp, d->mParserErrorString );
346+
d->mRootNode = ::parseExpression( d->mExp, d->mParserErrorString, d->mParserError );
342347
}
343348

344349
if ( !d->mRootNode )

src/core/expression/qgsexpression.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,49 @@ class CORE_EXPORT QgsExpression
115115
Q_DECLARE_TR_FUNCTIONS( QgsExpression )
116116
public:
117117

118+
/**
119+
* Details about any parser errors that were found when parsing the expression.
120+
* \since QGIS 3.0
121+
*/
122+
struct CORE_EXPORT ParserError
123+
{
124+
enum ParserErrorType
125+
{
126+
Unknown = 0, //!< Unknown error type.
127+
FunctionUnknown = 1, //!< Function was unknown.
128+
FunctionWrongArgs = 2, //!< Function was called with the wrong number of args.
129+
FunctionInvalidParams = 3, //!< Function was called with invalid args.
130+
FunctionNamedArgsError = 4 //!< Non named function arg used after named arg.
131+
};
132+
133+
/**
134+
* The type of parser error that was found.
135+
*/
136+
ParserErrorType errorType = ParserErrorType::Unknown;
137+
138+
/**
139+
* The first line that contained the error in the parser.
140+
* Depending on the error sometimes this doesn't mean anything.
141+
*/
142+
int firstLine = 0;
143+
144+
/**
145+
* The first column that contained the error in the parser.
146+
* Depending on the error sometimes this doesn't mean anything.
147+
*/
148+
int firstColumn = 0;
149+
150+
/**
151+
* The last line that contained the error in the parser.
152+
*/
153+
int lastLine = 0;
154+
155+
/**
156+
* The last column that contained the error in the parser.
157+
*/
158+
int lastColumn = 0;
159+
};
160+
118161
/**
119162
* Creates a new expression based on the provided string.
120163
* The string will immediately be parsed. For optimization
@@ -174,6 +217,12 @@ class CORE_EXPORT QgsExpression
174217
//! Returns parser error
175218
QString parserErrorString() const;
176219

220+
/**
221+
* Returns parser error details including location of error.
222+
* \since QGIS 3.0
223+
*/
224+
ParserError parserError() const;
225+
177226
//! Returns root node of the expression. Root node is null is parsing has failed
178227
const QgsExpressionNode *rootNode() const;
179228

src/core/qgsexpressionlexer.ll

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020
%option prefix="exp_"
2121
// this makes flex generate lexer with context + init/destroy functions
2222
%option reentrant
23+
%option yylineno
2324
// this makes Bison send yylex another argument to use instead of using the global variable yylval
2425
%option bison-bridge
26+
%option bison-locations
27+
2528

2629
// ensure that lexer will be 8-bit (and not just 7-bit)
2730
%option 8bit
@@ -54,6 +57,19 @@
5457
#define TEXT yylval->text = new QString( QString::fromUtf8(yytext) );
5558
#define TEXT_FILTER(filter_fn) yylval->text = new QString( filter_fn( QString::fromUtf8(yytext) ) );
5659

60+
#define YY_USER_ACTION \
61+
yylloc->first_line = yylloc->last_line; \
62+
yylloc->first_column = yylloc->last_column; \
63+
for(int i = 0; yytext[i] != '\0'; i++) { \
64+
if(yytext[i] == '\n') { \
65+
yylloc->last_line++; \
66+
yylloc->last_column = 0; \
67+
} \
68+
else { \
69+
yylloc->last_column++; \
70+
} \
71+
}
72+
5773
static QString stripText(QString text)
5874
{
5975
// strip single quotes on start,end

src/core/qgsexpressionparser.yy

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ typedef void* yyscan_t;
3838
typedef struct yy_buffer_state* YY_BUFFER_STATE;
3939
extern int exp_lex_init(yyscan_t* scanner);
4040
extern int exp_lex_destroy(yyscan_t scanner);
41-
extern int exp_lex(YYSTYPE* yylval_param, yyscan_t yyscanner);
41+
extern int exp_lex(YYSTYPE* yylval_param, YYLTYPE* yyloc, yyscan_t yyscanner);
4242
extern YY_BUFFER_STATE exp__scan_string(const char* buffer, yyscan_t scanner);
4343

4444
/** returns parsed tree, otherwise returns nullptr and sets parserErrorMsg
4545
(interface function to be called from QgsExpression)
4646
*/
47-
QgsExpressionNode* parseExpression(const QString& str, QString& parserErrorMsg);
47+
QgsExpressionNode* parseExpression(const QString& str, QString& parserErrorMsg, QgsExpression::ParserError& parserError);
4848

4949
/** error handler for bison */
50-
void exp_error(expression_parser_context* parser_ctx, const char* msg);
50+
void exp_error(YYLTYPE* yyloc, expression_parser_context* parser_ctx, const char* msg);
5151

5252
struct expression_parser_context
5353
{
@@ -56,6 +56,7 @@ struct expression_parser_context
5656

5757
// varible where the parser error will be stored
5858
QString errorMsg;
59+
QgsExpression::ParserError parserError;
5960
// root node of the expression
6061
QgsExpressionNode* rootNode;
6162
};
@@ -70,6 +71,7 @@ struct expression_parser_context
7071
%}
7172

7273
// make the parser reentrant
74+
%locations
7375
%define api.pure
7476
%lex-param {void * scanner}
7577
%parse-param {expression_parser_context* parser_ctx}
@@ -193,22 +195,28 @@ expression:
193195
{
194196
// this should not actually happen because already in lexer we check whether an identifier is a known function
195197
// (if the name is not known the token is parsed as a column)
196-
exp_error(parser_ctx, "Function is not known");
198+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionUnknown;
199+
parser_ctx->parserError.errorType = errorType;
200+
exp_error(&yyloc, parser_ctx, "Function is not known");
197201
delete $3;
198202
YYERROR;
199203
}
200204
QString paramError;
201205
if ( !QgsExpressionNodeFunction::validateParams( fnIndex, $3, paramError ) )
202206
{
203-
exp_error( parser_ctx, paramError.toLocal8Bit().constData() );
207+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionInvalidParams;
208+
parser_ctx->parserError.errorType = errorType;
209+
exp_error( &yyloc, parser_ctx, paramError.toLocal8Bit().constData() );
204210
delete $3;
205211
YYERROR;
206212
}
207213
if ( QgsExpression::Functions()[fnIndex]->params() != -1
208214
&& !( QgsExpression::Functions()[fnIndex]->params() >= $3->count()
209215
&& QgsExpression::Functions()[fnIndex]->minParams() <= $3->count() ) )
210216
{
211-
exp_error(parser_ctx, QString( "%1 function is called with wrong number of arguments" ).arg( QgsExpression::Functions()[fnIndex]->name() ).toLocal8Bit().constData() );
217+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionWrongArgs;
218+
parser_ctx->parserError.errorType = errorType;
219+
exp_error(&yyloc, parser_ctx, QString( "%1 function is called with wrong number of arguments" ).arg( QgsExpression::Functions()[fnIndex]->name() ).toLocal8Bit().constData() );
212220
delete $3;
213221
YYERROR;
214222
}
@@ -223,14 +231,18 @@ expression:
223231
{
224232
// this should not actually happen because already in lexer we check whether an identifier is a known function
225233
// (if the name is not known the token is parsed as a column)
226-
exp_error(parser_ctx, "Function is not known");
234+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionUnknown;
235+
parser_ctx->parserError.errorType = errorType;
236+
exp_error(&yyloc, parser_ctx, "Function is not known");
227237
YYERROR;
228238
}
229239
// 0 parameters is expected, -1 parameters means leave it to the
230240
// implementation
231241
if ( QgsExpression::Functions()[fnIndex]->params() > 0 )
232242
{
233-
exp_error(parser_ctx, QString( "%1 function is called with wrong number of arguments" ).arg( QgsExpression::Functions()[fnIndex]->name() ).toLocal8Bit().constData() );
243+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionWrongArgs;
244+
parser_ctx->parserError.errorType = errorType;
245+
exp_error(&yyloc, parser_ctx, QString( "%1 function is called with wrong number of arguments" ).arg( QgsExpression::Functions()[fnIndex]->name() ).toLocal8Bit().constData() );
234246
YYERROR;
235247
}
236248
$$ = new QgsExpressionNodeFunction(fnIndex, new QgsExpressionNode::NodeList());
@@ -258,7 +270,9 @@ expression:
258270
}
259271
else
260272
{
261-
exp_error(parser_ctx, QString("%1 function is not known").arg(*$1).toLocal8Bit().constData());
273+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionUnknown;
274+
parser_ctx->parserError.errorType = errorType;
275+
exp_error(&yyloc, parser_ctx, QString("%1 function is not known").arg(*$1).toLocal8Bit().constData());
262276
YYERROR;
263277
}
264278
delete $1;
@@ -292,7 +306,9 @@ exp_list:
292306
{
293307
if ( $1->hasNamedNodes() )
294308
{
295-
exp_error(parser_ctx, "All parameters following a named parameter must also be named.");
309+
QgsExpression::ParserError::ParserErrorType errorType = QgsExpression::ParserError::FunctionNamedArgsError;
310+
parser_ctx->parserError.errorType = errorType;
311+
exp_error(&yyloc, parser_ctx, "All parameters following a named parameter must also be named.");
296312
delete $1;
297313
YYERROR;
298314
}
@@ -319,7 +335,7 @@ when_then_clause:
319335

320336

321337
// returns parsed tree, otherwise returns nullptr and sets parserErrorMsg
322-
QgsExpressionNode* parseExpression(const QString& str, QString& parserErrorMsg)
338+
QgsExpressionNode* parseExpression(const QString& str, QString& parserErrorMsg, QgsExpression::ParserError &parserError)
323339
{
324340
expression_parser_context ctx;
325341
ctx.rootNode = 0;
@@ -337,13 +353,18 @@ QgsExpressionNode* parseExpression(const QString& str, QString& parserErrorMsg)
337353
else // error?
338354
{
339355
parserErrorMsg = ctx.errorMsg;
356+
parserError = ctx.parserError;
340357
delete ctx.rootNode;
341358
return nullptr;
342359
}
343360
}
344361

345362

346-
void exp_error(expression_parser_context* parser_ctx, const char* msg)
363+
void exp_error(YYLTYPE* yyloc,expression_parser_context* parser_ctx, const char* msg)
347364
{
348365
parser_ctx->errorMsg = msg;
366+
parser_ctx->parserError.firstColumn = yyloc->first_column;
367+
parser_ctx->parserError.firstLine = yyloc->first_line;
368+
parser_ctx->parserError.lastColumn = yyloc->last_column;
369+
parser_ctx->parserError.lastLine = yyloc->last_line;
349370
}

src/core/qgsexpressionprivate.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class QgsExpressionPrivate
4444
, mRootNode( other.mRootNode ? other.mRootNode->clone() : nullptr )
4545
, mParserErrorString( other.mParserErrorString )
4646
, mEvalErrorString( other.mEvalErrorString )
47+
, mParserError( other.mParserError )
4748
, mExp( other.mExp )
4849
, mCalc( other.mCalc )
4950
, mDistanceUnit( other.mDistanceUnit )
@@ -62,6 +63,8 @@ class QgsExpressionPrivate
6263
QString mParserErrorString;
6364
QString mEvalErrorString;
6465

66+
QgsExpression::ParserError mParserError;
67+
6568
QString mExp;
6669

6770
std::shared_ptr<QgsDistanceArea> mCalc;

0 commit comments

Comments
 (0)