mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Implement type checking/validation for filters (#4364)
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
c8e1741e47
commit
34db692895
|
@ -7,6 +7,7 @@
|
|||
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
|
||||
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
|
||||
- Minor: Added the ability to reply to a message by `Shift + Right Click`ing the username. (#4424)
|
||||
- Minor: Added better filter validation and error messages. (#4364)
|
||||
- Minor: Updated the look of the Black Theme to be more in line with the other themes. (#4523)
|
||||
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
||||
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
||||
|
|
|
@ -74,12 +74,26 @@ set(SOURCE_FILES
|
|||
controllers/filters/FilterRecord.hpp
|
||||
controllers/filters/FilterSet.cpp
|
||||
controllers/filters/FilterSet.hpp
|
||||
controllers/filters/parser/FilterParser.cpp
|
||||
controllers/filters/parser/FilterParser.hpp
|
||||
controllers/filters/parser/Tokenizer.cpp
|
||||
controllers/filters/parser/Tokenizer.hpp
|
||||
controllers/filters/parser/Types.cpp
|
||||
controllers/filters/parser/Types.hpp
|
||||
controllers/filters/lang/expressions/Expression.cpp
|
||||
controllers/filters/lang/expressions/Expression.hpp
|
||||
controllers/filters/lang/expressions/BinaryOperation.cpp
|
||||
controllers/filters/lang/expressions/BinaryOperation.hpp
|
||||
controllers/filters/lang/expressions/ListExpression.cpp
|
||||
controllers/filters/lang/expressions/ListExpression.hpp
|
||||
controllers/filters/lang/expressions/RegexExpression.cpp
|
||||
controllers/filters/lang/expressions/RegexExpression.hpp
|
||||
controllers/filters/lang/expressions/UnaryOperation.hpp
|
||||
controllers/filters/lang/expressions/UnaryOperation.cpp
|
||||
controllers/filters/lang/expressions/ValueExpression.cpp
|
||||
controllers/filters/lang/expressions/ValueExpression.hpp
|
||||
controllers/filters/lang/Filter.cpp
|
||||
controllers/filters/lang/Filter.hpp
|
||||
controllers/filters/lang/FilterParser.cpp
|
||||
controllers/filters/lang/FilterParser.hpp
|
||||
controllers/filters/lang/Tokenizer.cpp
|
||||
controllers/filters/lang/Tokenizer.hpp
|
||||
controllers/filters/lang/Types.cpp
|
||||
controllers/filters/lang/Types.hpp
|
||||
|
||||
controllers/highlights/BadgeHighlightModel.cpp
|
||||
controllers/highlights/BadgeHighlightModel.hpp
|
||||
|
|
|
@ -1,21 +1,40 @@
|
|||
#include "controllers/filters/FilterRecord.hpp"
|
||||
|
||||
#include "controllers/filters/lang/Filter.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
FilterRecord::FilterRecord(const QString &name, const QString &filter)
|
||||
: name_(name)
|
||||
, filter_(filter)
|
||||
, id_(QUuid::createUuid())
|
||||
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
||||
static std::unique_ptr<filters::Filter> buildFilter(const QString &filterText)
|
||||
{
|
||||
using namespace filters;
|
||||
auto result = Filter::fromString(filterText);
|
||||
if (std::holds_alternative<Filter>(result))
|
||||
{
|
||||
auto filter =
|
||||
std::make_unique<Filter>(std::move(std::get<Filter>(result)));
|
||||
|
||||
if (filter->returnType() != Type::Bool)
|
||||
{
|
||||
// Only accept Bool results
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FilterRecord::FilterRecord(QString name, QString filter)
|
||||
: FilterRecord(std::move(name), std::move(filter), QUuid::createUuid())
|
||||
{
|
||||
}
|
||||
|
||||
FilterRecord::FilterRecord(const QString &name, const QString &filter,
|
||||
const QUuid &id)
|
||||
: name_(name)
|
||||
, filter_(filter)
|
||||
FilterRecord::FilterRecord(QString name, QString filter, const QUuid &id)
|
||||
: name_(std::move(name))
|
||||
, filterText_(std::move(filter))
|
||||
, id_(id)
|
||||
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
||||
, filter_(buildFilter(this->filterText_))
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -26,7 +45,7 @@ const QString &FilterRecord::getName() const
|
|||
|
||||
const QString &FilterRecord::getFilter() const
|
||||
{
|
||||
return this->filter_;
|
||||
return this->filterText_;
|
||||
}
|
||||
|
||||
const QUuid &FilterRecord::getId() const
|
||||
|
@ -36,12 +55,13 @@ const QUuid &FilterRecord::getId() const
|
|||
|
||||
bool FilterRecord::valid() const
|
||||
{
|
||||
return this->parser_->valid();
|
||||
return this->filter_ != nullptr;
|
||||
}
|
||||
|
||||
bool FilterRecord::filter(const filterparser::ContextMap &context) const
|
||||
bool FilterRecord::filter(const filters::ContextMap &context) const
|
||||
{
|
||||
return this->parser_->execute(context);
|
||||
assert(this->valid());
|
||||
return this->filter_->execute(context).toBool();
|
||||
}
|
||||
|
||||
bool FilterRecord::operator==(const FilterRecord &other) const
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/parser/FilterParser.hpp"
|
||||
#include "controllers/filters/lang/Filter.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
|
||||
|
@ -16,9 +16,9 @@ namespace chatterino {
|
|||
class FilterRecord
|
||||
{
|
||||
public:
|
||||
FilterRecord(const QString &name, const QString &filter);
|
||||
FilterRecord(QString name, QString filter);
|
||||
|
||||
FilterRecord(const QString &name, const QString &filter, const QUuid &id);
|
||||
FilterRecord(QString name, QString filter, const QUuid &id);
|
||||
|
||||
const QString &getName() const;
|
||||
|
||||
|
@ -28,16 +28,16 @@ public:
|
|||
|
||||
bool valid() const;
|
||||
|
||||
bool filter(const filterparser::ContextMap &context) const;
|
||||
bool filter(const filters::ContextMap &context) const;
|
||||
|
||||
bool operator==(const FilterRecord &other) const;
|
||||
|
||||
private:
|
||||
QString name_;
|
||||
QString filter_;
|
||||
QUuid id_;
|
||||
const QString name_;
|
||||
const QString filterText_;
|
||||
const QUuid id_;
|
||||
|
||||
std::unique_ptr<filterparser::FilterParser> parser_;
|
||||
const std::unique_ptr<filters::Filter> filter_;
|
||||
};
|
||||
|
||||
using FilterRecordPtr = std::shared_ptr<FilterRecord>;
|
||||
|
|
|
@ -38,8 +38,7 @@ bool FilterSet::filter(const MessagePtr &m, ChannelPtr channel) const
|
|||
if (this->filters_.size() == 0)
|
||||
return true;
|
||||
|
||||
filterparser::ContextMap context =
|
||||
filterparser::buildContextMap(m, channel.get());
|
||||
filters::ContextMap context = filters::buildContextMap(m, channel.get());
|
||||
for (const auto &f : this->filters_.values())
|
||||
{
|
||||
if (!f->valid() || !f->filter(context))
|
||||
|
|
161
src/controllers/filters/lang/Filter.cpp
Normal file
161
src/controllers/filters/lang/Filter.cpp
Normal file
|
@ -0,0 +1,161 @@
|
|||
#include "controllers/filters/lang/Filter.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "controllers/filters/lang/FilterParser.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||
{
|
||||
auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get();
|
||||
|
||||
/*
|
||||
* Looking to add a new identifier to filters? Here's what to do:
|
||||
* 1. Update validIdentifiersMap in Tokenizer.hpp
|
||||
* 2. Add the identifier to the list below
|
||||
* 3. Add the type of the identifier to MESSAGE_TYPING_CONTEXT in Filter.hpp
|
||||
* 4. Add the value for the identifier to the ContextMap returned by this function
|
||||
*
|
||||
* List of identifiers:
|
||||
*
|
||||
* author.badges
|
||||
* author.color
|
||||
* author.name
|
||||
* author.no_color
|
||||
* author.subbed
|
||||
* author.sub_length
|
||||
*
|
||||
* channel.name
|
||||
* channel.watching
|
||||
*
|
||||
* flags.highlighted
|
||||
* flags.points_redeemed
|
||||
* flags.sub_message
|
||||
* flags.system_message
|
||||
* flags.reward_message
|
||||
* flags.first_message
|
||||
* flags.elevated_message
|
||||
* flags.cheer_message
|
||||
* flags.whisper
|
||||
* flags.reply
|
||||
* flags.automod
|
||||
*
|
||||
* message.content
|
||||
* message.length
|
||||
*
|
||||
*/
|
||||
|
||||
using MessageFlag = chatterino::MessageFlag;
|
||||
|
||||
QStringList badges;
|
||||
badges.reserve(m->badges.size());
|
||||
for (const auto &e : m->badges)
|
||||
{
|
||||
badges << e.key_;
|
||||
}
|
||||
|
||||
bool watching = !watchingChannel->getName().isEmpty() &&
|
||||
watchingChannel->getName().compare(
|
||||
m->channelName, Qt::CaseInsensitive) == 0;
|
||||
|
||||
bool subscribed = false;
|
||||
int subLength = 0;
|
||||
for (const auto &subBadge : {"subscriber", "founder"})
|
||||
{
|
||||
if (!badges.contains(subBadge))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
subscribed = true;
|
||||
if (m->badgeInfos.find(subBadge) != m->badgeInfos.end())
|
||||
{
|
||||
subLength = m->badgeInfos.at(subBadge).toInt();
|
||||
}
|
||||
}
|
||||
ContextMap vars = {
|
||||
{"author.badges", std::move(badges)},
|
||||
{"author.color", m->usernameColor},
|
||||
{"author.name", m->displayName},
|
||||
{"author.no_color", !m->usernameColor.isValid()},
|
||||
{"author.subbed", subscribed},
|
||||
{"author.sub_length", subLength},
|
||||
|
||||
{"channel.name", m->channelName},
|
||||
{"channel.watching", watching},
|
||||
|
||||
{"flags.highlighted", m->flags.has(MessageFlag::Highlighted)},
|
||||
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
||||
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
||||
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
||||
{"flags.reward_message",
|
||||
m->flags.has(MessageFlag::RedeemedChannelPointReward)},
|
||||
{"flags.first_message", m->flags.has(MessageFlag::FirstMessage)},
|
||||
{"flags.elevated_message", m->flags.has(MessageFlag::ElevatedMessage)},
|
||||
{"flags.cheer_message", m->flags.has(MessageFlag::CheerMessage)},
|
||||
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
|
||||
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
|
||||
|
||||
{"message.content", m->messageText},
|
||||
{"message.length", m->messageText.length()},
|
||||
};
|
||||
{
|
||||
auto *tc = dynamic_cast<TwitchChannel *>(channel);
|
||||
if (channel && !channel->isEmpty() && tc)
|
||||
{
|
||||
vars["channel.live"] = tc->isLive();
|
||||
}
|
||||
else
|
||||
{
|
||||
vars["channel.live"] = false;
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
|
||||
FilterResult Filter::fromString(const QString &str)
|
||||
{
|
||||
FilterParser parser(str);
|
||||
|
||||
if (parser.valid())
|
||||
{
|
||||
auto exp = parser.release();
|
||||
auto typ = parser.returnType();
|
||||
return Filter(std::move(exp), typ);
|
||||
}
|
||||
|
||||
return FilterError{parser.errors().join("\n")};
|
||||
}
|
||||
|
||||
Filter::Filter(ExpressionPtr expression, Type returnType)
|
||||
: expression_(std::move(expression))
|
||||
, returnType_(returnType)
|
||||
{
|
||||
}
|
||||
|
||||
Type Filter::returnType() const
|
||||
{
|
||||
return this->returnType_;
|
||||
}
|
||||
|
||||
QVariant Filter::execute(const ContextMap &context) const
|
||||
{
|
||||
return this->expression_->execute(context);
|
||||
}
|
||||
|
||||
QString Filter::filterString() const
|
||||
{
|
||||
return this->expression_->filterString();
|
||||
}
|
||||
|
||||
QString Filter::debugString(const TypingContext &context) const
|
||||
{
|
||||
return this->expression_->debug(context);
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
77
src/controllers/filters/lang/Filter.hpp
Normal file
77
src/controllers/filters/lang/Filter.hpp
Normal file
|
@ -0,0 +1,77 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Channel;
|
||||
struct Message;
|
||||
using MessagePtr = std::shared_ptr<const Message>;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
// MESSAGE_TYPING_CONTEXT maps filter variables to their expected type at evaluation.
|
||||
// For example, flags.highlighted is a boolean variable, so it is marked as Type::Bool
|
||||
// below. These variable types will be used to check whether a filter "makes sense",
|
||||
// i.e. if all the variables and operators being used have compatible types.
|
||||
static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
|
||||
{"author.badges", Type::StringList},
|
||||
{"author.color", Type::Color},
|
||||
{"author.name", Type::String},
|
||||
{"author.no_color", Type::Bool},
|
||||
{"author.subbed", Type::Bool},
|
||||
{"author.sub_length", Type::Int},
|
||||
{"channel.name", Type::String},
|
||||
{"channel.watching", Type::Bool},
|
||||
{"channel.live", Type::Bool},
|
||||
{"flags.highlighted", Type::Bool},
|
||||
{"flags.points_redeemed", Type::Bool},
|
||||
{"flags.sub_message", Type::Bool},
|
||||
{"flags.system_message", Type::Bool},
|
||||
{"flags.reward_message", Type::Bool},
|
||||
{"flags.first_message", Type::Bool},
|
||||
{"flags.elevated_message", Type::Bool},
|
||||
{"flags.cheer_message", Type::Bool},
|
||||
{"flags.whisper", Type::Bool},
|
||||
{"flags.reply", Type::Bool},
|
||||
{"flags.automod", Type::Bool},
|
||||
{"message.content", Type::String},
|
||||
{"message.length", Type::Int},
|
||||
};
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);
|
||||
|
||||
class Filter;
|
||||
struct FilterError {
|
||||
QString message;
|
||||
};
|
||||
|
||||
using FilterResult = std::variant<Filter, FilterError>;
|
||||
|
||||
class Filter
|
||||
{
|
||||
public:
|
||||
static FilterResult fromString(const QString &str);
|
||||
|
||||
Type returnType() const;
|
||||
QVariant execute(const ContextMap &context) const;
|
||||
|
||||
QString filterString() const;
|
||||
QString debugString(const TypingContext &context) const;
|
||||
|
||||
private:
|
||||
Filter(ExpressionPtr expression, Type returnType);
|
||||
|
||||
ExpressionPtr expression_;
|
||||
Type returnType_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::filters
|
|
@ -1,115 +1,21 @@
|
|||
#include "FilterParser.hpp"
|
||||
#include "controllers/filters/lang/FilterParser.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "controllers/filters/lang/expressions/BinaryOperation.hpp"
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/expressions/ListExpression.hpp"
|
||||
#include "controllers/filters/lang/expressions/RegexExpression.hpp"
|
||||
#include "controllers/filters/lang/expressions/UnaryOperation.hpp"
|
||||
#include "controllers/filters/lang/expressions/ValueExpression.hpp"
|
||||
#include "controllers/filters/lang/Filter.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
namespace chatterino::filters {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||
QString explainIllType(const IllTyped &ill)
|
||||
{
|
||||
auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get();
|
||||
|
||||
/* Known Identifiers
|
||||
*
|
||||
* author.badges
|
||||
* author.color
|
||||
* author.name
|
||||
* author.no_color
|
||||
* author.subbed
|
||||
* author.sub_length
|
||||
*
|
||||
* channel.name
|
||||
* channel.watching
|
||||
*
|
||||
* flags.highlighted
|
||||
* flags.points_redeemed
|
||||
* flags.sub_message
|
||||
* flags.system_message
|
||||
* flags.reward_message
|
||||
* flags.first_message
|
||||
* flags.elevated_message
|
||||
* flags.cheer_message
|
||||
* flags.whisper
|
||||
* flags.reply
|
||||
* flags.automod
|
||||
*
|
||||
* message.content
|
||||
* message.length
|
||||
*
|
||||
*/
|
||||
|
||||
using MessageFlag = chatterino::MessageFlag;
|
||||
|
||||
QStringList badges;
|
||||
badges.reserve(m->badges.size());
|
||||
for (const auto &e : m->badges)
|
||||
{
|
||||
badges << e.key_;
|
||||
}
|
||||
|
||||
bool watching = !watchingChannel->getName().isEmpty() &&
|
||||
watchingChannel->getName().compare(
|
||||
m->channelName, Qt::CaseInsensitive) == 0;
|
||||
|
||||
bool subscribed = false;
|
||||
int subLength = 0;
|
||||
for (const auto &subBadge : {"subscriber", "founder"})
|
||||
{
|
||||
if (!badges.contains(subBadge))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
subscribed = true;
|
||||
if (m->badgeInfos.find(subBadge) != m->badgeInfos.end())
|
||||
{
|
||||
subLength = m->badgeInfos.at(subBadge).toInt();
|
||||
}
|
||||
}
|
||||
ContextMap vars = {
|
||||
{"author.badges", std::move(badges)},
|
||||
{"author.color", m->usernameColor},
|
||||
{"author.name", m->displayName},
|
||||
{"author.no_color", !m->usernameColor.isValid()},
|
||||
{"author.subbed", subscribed},
|
||||
{"author.sub_length", subLength},
|
||||
|
||||
{"channel.name", m->channelName},
|
||||
{"channel.watching", watching},
|
||||
|
||||
{"flags.highlighted", m->flags.has(MessageFlag::Highlighted)},
|
||||
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
||||
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
||||
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
||||
{"flags.reward_message",
|
||||
m->flags.has(MessageFlag::RedeemedChannelPointReward)},
|
||||
{"flags.first_message", m->flags.has(MessageFlag::FirstMessage)},
|
||||
{"flags.elevated_message", m->flags.has(MessageFlag::ElevatedMessage)},
|
||||
{"flags.cheer_message", m->flags.has(MessageFlag::CheerMessage)},
|
||||
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
|
||||
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
|
||||
|
||||
{"message.content", m->messageText},
|
||||
{"message.length", m->messageText.length()},
|
||||
};
|
||||
{
|
||||
using namespace chatterino;
|
||||
auto *tc = dynamic_cast<TwitchChannel *>(channel);
|
||||
if (channel && !channel->isEmpty() && tc)
|
||||
{
|
||||
vars["channel.live"] = tc->isLive();
|
||||
}
|
||||
else
|
||||
{
|
||||
vars["channel.live"] = false;
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
return QString("%1\n\nProblem occurred here:\n%2")
|
||||
.arg(ill.message)
|
||||
.arg(ill.expr->filterString());
|
||||
}
|
||||
|
||||
FilterParser::FilterParser(const QString &text)
|
||||
|
@ -117,11 +23,22 @@ FilterParser::FilterParser(const QString &text)
|
|||
, tokenizer_(Tokenizer(text))
|
||||
, builtExpression_(this->parseExpression(true))
|
||||
{
|
||||
}
|
||||
if (!this->valid_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool FilterParser::execute(const ContextMap &context) const
|
||||
{
|
||||
return this->builtExpression_->execute(context).toBool();
|
||||
// safety: returnType must not live longer than the parsed expression. See
|
||||
// comment on IllTyped::expr.
|
||||
auto returnType =
|
||||
this->builtExpression_->synthesizeType(MESSAGE_TYPING_CONTEXT);
|
||||
if (isIllTyped(returnType))
|
||||
{
|
||||
this->errorLog(explainIllType(std::get<IllTyped>(returnType)));
|
||||
return;
|
||||
}
|
||||
|
||||
this->returnType_ = std::get<TypeClass>(returnType).type;
|
||||
}
|
||||
|
||||
bool FilterParser::valid() const
|
||||
|
@ -129,6 +46,18 @@ bool FilterParser::valid() const
|
|||
return this->valid_;
|
||||
}
|
||||
|
||||
Type FilterParser::returnType() const
|
||||
{
|
||||
return this->returnType_;
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::release()
|
||||
{
|
||||
ExpressionPtr ret;
|
||||
this->builtExpression_.swap(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseExpression(bool top)
|
||||
{
|
||||
auto e = this->parseAnd();
|
||||
|
@ -379,12 +308,7 @@ const QStringList &FilterParser::errors() const
|
|||
|
||||
const QString FilterParser::debugString() const
|
||||
{
|
||||
return this->builtExpression_->debug();
|
||||
return this->builtExpression_->debug(MESSAGE_TYPING_CONTEXT);
|
||||
}
|
||||
|
||||
const QString FilterParser::filterString() const
|
||||
{
|
||||
return this->builtExpression_->filterString();
|
||||
}
|
||||
|
||||
} // namespace filterparser
|
||||
} // namespace chatterino::filters
|
|
@ -1,28 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Channel;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);
|
||||
namespace chatterino::filters {
|
||||
|
||||
class FilterParser
|
||||
{
|
||||
public:
|
||||
FilterParser(const QString &text);
|
||||
bool execute(const ContextMap &context) const;
|
||||
|
||||
bool valid() const;
|
||||
Type returnType() const;
|
||||
ExpressionPtr release();
|
||||
|
||||
const QStringList &errors() const;
|
||||
const QString debugString() const;
|
||||
const QString filterString() const;
|
||||
|
||||
private:
|
||||
ExpressionPtr parseExpression(bool top = false);
|
||||
|
@ -41,5 +35,7 @@ private:
|
|||
QString text_;
|
||||
Tokenizer tokenizer_;
|
||||
ExpressionPtr builtExpression_;
|
||||
Type returnType_ = Type::Bool;
|
||||
};
|
||||
} // namespace filterparser
|
||||
|
||||
} // namespace chatterino::filters
|
|
@ -1,8 +1,79 @@
|
|||
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||
|
||||
#include "common/QLogging.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
namespace chatterino::filters {
|
||||
|
||||
QString tokenTypeToInfoString(TokenType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AND:
|
||||
return "And";
|
||||
case OR:
|
||||
return "Or";
|
||||
case LP:
|
||||
return "<left parenthesis>";
|
||||
case RP:
|
||||
return "<right parenthesis>";
|
||||
case LIST_START:
|
||||
return "<list start>";
|
||||
case LIST_END:
|
||||
return "<list end>";
|
||||
case COMMA:
|
||||
return "<comma>";
|
||||
case PLUS:
|
||||
return "Plus";
|
||||
case MINUS:
|
||||
return "Minus";
|
||||
case MULTIPLY:
|
||||
return "Multiply";
|
||||
case DIVIDE:
|
||||
return "Divide";
|
||||
case MOD:
|
||||
return "Mod";
|
||||
case EQ:
|
||||
return "Eq";
|
||||
case NEQ:
|
||||
return "NotEq";
|
||||
case LT:
|
||||
return "LessThan";
|
||||
case GT:
|
||||
return "GreaterThan";
|
||||
case LTE:
|
||||
return "LessThanEq";
|
||||
case GTE:
|
||||
return "GreaterThanEq";
|
||||
case CONTAINS:
|
||||
return "Contains";
|
||||
case STARTS_WITH:
|
||||
return "StartsWith";
|
||||
case ENDS_WITH:
|
||||
return "EndsWith";
|
||||
case MATCH:
|
||||
return "Match";
|
||||
case NOT:
|
||||
return "Not";
|
||||
case STRING:
|
||||
return "<string>";
|
||||
case INT:
|
||||
return "<int>";
|
||||
case IDENTIFIER:
|
||||
return "<identifier>";
|
||||
case CONTROL_START:
|
||||
case CONTROL_END:
|
||||
case BINARY_START:
|
||||
case BINARY_END:
|
||||
case UNARY_START:
|
||||
case UNARY_END:
|
||||
case MATH_START:
|
||||
case MATH_END:
|
||||
case OTHER_START:
|
||||
case NONE:
|
||||
default:
|
||||
return "<unknown>";
|
||||
}
|
||||
}
|
||||
|
||||
Tokenizer::Tokenizer(const QString &text)
|
||||
{
|
||||
|
@ -190,4 +261,4 @@ bool Tokenizer::typeIsMathOp(TokenType token)
|
|||
return token > TokenType::MATH_START && token < TokenType::MATH_END;
|
||||
}
|
||||
|
||||
} // namespace filterparser
|
||||
} // namespace chatterino::filters
|
|
@ -1,12 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
#include <QMap>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
|
||||
namespace filterparser {
|
||||
namespace chatterino::filters {
|
||||
|
||||
static const QMap<QString, QString> validIdentifiersMap = {
|
||||
{"author.badges", "author badges"},
|
||||
|
@ -17,7 +17,7 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
|||
{"author.sub_length", "author sub length"},
|
||||
{"channel.name", "channel name"},
|
||||
{"channel.watching", "/watching channel?"},
|
||||
{"channel.live", "Channel live?"},
|
||||
{"channel.live", "channel live?"},
|
||||
{"flags.highlighted", "highlighted?"},
|
||||
{"flags.points_redeemed", "redeemed points?"},
|
||||
{"flags.sub_message", "sub/resub message?"},
|
||||
|
@ -42,6 +42,58 @@ static const QRegularExpression tokenRegex(
|
|||
);
|
||||
// clang-format on
|
||||
|
||||
enum TokenType {
|
||||
// control
|
||||
CONTROL_START = 0,
|
||||
AND = 1,
|
||||
OR = 2,
|
||||
LP = 3,
|
||||
RP = 4,
|
||||
LIST_START = 5,
|
||||
LIST_END = 6,
|
||||
COMMA = 7,
|
||||
CONTROL_END = 19,
|
||||
|
||||
// binary operator
|
||||
BINARY_START = 20,
|
||||
EQ = 21,
|
||||
NEQ = 22,
|
||||
LT = 23,
|
||||
GT = 24,
|
||||
LTE = 25,
|
||||
GTE = 26,
|
||||
CONTAINS = 27,
|
||||
STARTS_WITH = 28,
|
||||
ENDS_WITH = 29,
|
||||
MATCH = 30,
|
||||
BINARY_END = 49,
|
||||
|
||||
// unary operator
|
||||
UNARY_START = 50,
|
||||
NOT = 51,
|
||||
UNARY_END = 99,
|
||||
|
||||
// math operators
|
||||
MATH_START = 100,
|
||||
PLUS = 101,
|
||||
MINUS = 102,
|
||||
MULTIPLY = 103,
|
||||
DIVIDE = 104,
|
||||
MOD = 105,
|
||||
MATH_END = 149,
|
||||
|
||||
// other types
|
||||
OTHER_START = 150,
|
||||
STRING = 151,
|
||||
INT = 152,
|
||||
IDENTIFIER = 153,
|
||||
REGULAR_EXPRESSION = 154,
|
||||
|
||||
NONE = 200
|
||||
};
|
||||
|
||||
QString tokenTypeToInfoString(TokenType type);
|
||||
|
||||
class Tokenizer
|
||||
{
|
||||
public:
|
||||
|
@ -74,4 +126,4 @@ private:
|
|||
|
||||
TokenType tokenize(const QString &text);
|
||||
};
|
||||
} // namespace filterparser
|
||||
} // namespace chatterino::filters
|
101
src/controllers/filters/lang/Types.cpp
Normal file
101
src/controllers/filters/lang/Types.cpp
Normal file
|
@ -0,0 +1,101 @@
|
|||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
bool isList(const PossibleType &possibleType)
|
||||
{
|
||||
using T = Type;
|
||||
if (isIllTyped(possibleType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto typ = std::get<TypeClass>(possibleType);
|
||||
return typ == T::List || typ == T::StringList ||
|
||||
typ == T::MatchingSpecifier;
|
||||
}
|
||||
|
||||
QString typeToString(Type type)
|
||||
{
|
||||
using T = Type;
|
||||
switch (type)
|
||||
{
|
||||
case T::String:
|
||||
return "String";
|
||||
case T::Int:
|
||||
return "Int";
|
||||
case T::Bool:
|
||||
return "Bool";
|
||||
case T::Color:
|
||||
return "Color";
|
||||
case T::RegularExpression:
|
||||
return "RegularExpression";
|
||||
case T::List:
|
||||
return "List";
|
||||
case T::StringList:
|
||||
return "StringList";
|
||||
case T::MatchingSpecifier:
|
||||
return "MatchingSpecifier";
|
||||
case T::Map:
|
||||
return "Map";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
QString TypeClass::string() const
|
||||
{
|
||||
return typeToString(this->type);
|
||||
}
|
||||
|
||||
bool TypeClass::operator==(Type t) const
|
||||
{
|
||||
return this->type == t;
|
||||
}
|
||||
|
||||
bool TypeClass::operator==(const TypeClass &t) const
|
||||
{
|
||||
return this->type == t.type;
|
||||
}
|
||||
|
||||
bool TypeClass::operator==(const IllTyped &t) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TypeClass::operator!=(Type t) const
|
||||
{
|
||||
return !this->operator==(t);
|
||||
}
|
||||
|
||||
bool TypeClass::operator!=(const TypeClass &t) const
|
||||
{
|
||||
return !this->operator==(t);
|
||||
}
|
||||
|
||||
bool TypeClass::operator!=(const IllTyped &t) const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
QString IllTyped::string() const
|
||||
{
|
||||
return "IllTyped";
|
||||
}
|
||||
|
||||
QString possibleTypeToString(const PossibleType &possible)
|
||||
{
|
||||
if (isWellTyped(possible))
|
||||
{
|
||||
return std::get<TypeClass>(possible).string();
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::get<IllTyped>(possible).string();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
95
src/controllers/filters/lang/Types.hpp
Normal file
95
src/controllers/filters/lang/Types.hpp
Normal file
|
@ -0,0 +1,95 @@
|
|||
#pragma once
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <variant>
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class Expression;
|
||||
|
||||
enum class Type {
|
||||
String,
|
||||
Int,
|
||||
Bool,
|
||||
Color,
|
||||
RegularExpression,
|
||||
List,
|
||||
StringList, // List of only strings
|
||||
MatchingSpecifier, // 2-element list in {RegularExpression, Int} form
|
||||
Map
|
||||
};
|
||||
|
||||
using ContextMap = QMap<QString, QVariant>;
|
||||
using TypingContext = QMap<QString, Type>;
|
||||
|
||||
QString typeToString(Type type);
|
||||
|
||||
struct IllTyped;
|
||||
|
||||
struct TypeClass {
|
||||
Type type;
|
||||
|
||||
QString string() const;
|
||||
|
||||
bool operator==(Type t) const;
|
||||
bool operator==(const TypeClass &t) const;
|
||||
bool operator==(const IllTyped &t) const;
|
||||
bool operator!=(Type t) const;
|
||||
bool operator!=(const TypeClass &t) const;
|
||||
bool operator!=(const IllTyped &t) const;
|
||||
};
|
||||
|
||||
struct IllTyped {
|
||||
// Important nuance to expr:
|
||||
// During type synthesis, should an error occur and an IllTyped PossibleType be
|
||||
// returned, expr is a pointer to an Expression that exists in the Expression
|
||||
// tree that was parsed. Therefore, you cannot hold on to this pointer longer
|
||||
// than the Expression tree exists. Be careful!
|
||||
const Expression *expr;
|
||||
QString message;
|
||||
|
||||
QString string() const;
|
||||
};
|
||||
|
||||
using PossibleType = std::variant<TypeClass, IllTyped>;
|
||||
|
||||
inline bool isWellTyped(const PossibleType &possible)
|
||||
{
|
||||
return std::holds_alternative<TypeClass>(possible);
|
||||
}
|
||||
|
||||
inline bool isIllTyped(const PossibleType &possible)
|
||||
{
|
||||
return std::holds_alternative<IllTyped>(possible);
|
||||
}
|
||||
|
||||
QString possibleTypeToString(const PossibleType &possible);
|
||||
|
||||
bool isList(const PossibleType &possibleType);
|
||||
|
||||
inline bool variantIs(const QVariant &a, QMetaType::Type type)
|
||||
{
|
||||
return static_cast<QMetaType::Type>(a.type()) == type;
|
||||
}
|
||||
|
||||
inline bool variantIsNot(const QVariant &a, QMetaType::Type type)
|
||||
{
|
||||
return static_cast<QMetaType::Type>(a.type()) != type;
|
||||
}
|
||||
|
||||
inline bool convertVariantTypes(QVariant &a, QVariant &b, int type)
|
||||
{
|
||||
return a.convert(type) && b.convert(type);
|
||||
}
|
||||
|
||||
inline bool variantTypesMatch(QVariant &a, QVariant &b, QMetaType::Type type)
|
||||
{
|
||||
return variantIs(a, type) && variantIs(b, type);
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
|
@ -1,214 +1,8 @@
|
|||
#include "controllers/filters/parser/Types.hpp"
|
||||
#include "controllers/filters/lang/expressions/BinaryOperation.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
#include <QRegularExpression>
|
||||
|
||||
bool convertVariantTypes(QVariant &a, QVariant &b, int type)
|
||||
{
|
||||
return a.convert(type) && b.convert(type);
|
||||
}
|
||||
|
||||
bool variantTypesMatch(QVariant &a, QVariant &b, QVariant::Type type)
|
||||
{
|
||||
return a.type() == type && b.type() == type;
|
||||
}
|
||||
|
||||
QString tokenTypeToInfoString(TokenType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case CONTROL_START:
|
||||
case CONTROL_END:
|
||||
case BINARY_START:
|
||||
case BINARY_END:
|
||||
case UNARY_START:
|
||||
case UNARY_END:
|
||||
case MATH_START:
|
||||
case MATH_END:
|
||||
case OTHER_START:
|
||||
case NONE:
|
||||
return "<unknown>";
|
||||
case AND:
|
||||
return "<and>";
|
||||
case OR:
|
||||
return "<or>";
|
||||
case LP:
|
||||
return "<left parenthesis>";
|
||||
case RP:
|
||||
return "<right parenthesis>";
|
||||
case LIST_START:
|
||||
return "<list start>";
|
||||
case LIST_END:
|
||||
return "<list end>";
|
||||
case COMMA:
|
||||
return "<comma>";
|
||||
case PLUS:
|
||||
return "<plus>";
|
||||
case MINUS:
|
||||
return "<minus>";
|
||||
case MULTIPLY:
|
||||
return "<multiply>";
|
||||
case DIVIDE:
|
||||
return "<divide>";
|
||||
case MOD:
|
||||
return "<modulus>";
|
||||
case EQ:
|
||||
return "<equals>";
|
||||
case NEQ:
|
||||
return "<not equals>";
|
||||
case LT:
|
||||
return "<less than>";
|
||||
case GT:
|
||||
return "<greater than>";
|
||||
case LTE:
|
||||
return "<less than equal>";
|
||||
case GTE:
|
||||
return "<greater than equal>";
|
||||
case CONTAINS:
|
||||
return "<contains>";
|
||||
case STARTS_WITH:
|
||||
return "<starts with>";
|
||||
case ENDS_WITH:
|
||||
return "<ends with>";
|
||||
case MATCH:
|
||||
return "<match>";
|
||||
case NOT:
|
||||
return "<not>";
|
||||
case STRING:
|
||||
return "<string>";
|
||||
case INT:
|
||||
return "<int>";
|
||||
case IDENTIFIER:
|
||||
return "<identifier>";
|
||||
default:
|
||||
return "<unknown>";
|
||||
}
|
||||
}
|
||||
|
||||
// ValueExpression
|
||||
|
||||
ValueExpression::ValueExpression(QVariant value, TokenType type)
|
||||
: value_(value)
|
||||
, type_(type){};
|
||||
|
||||
QVariant ValueExpression::execute(const ContextMap &context) const
|
||||
{
|
||||
if (this->type_ == TokenType::IDENTIFIER)
|
||||
{
|
||||
return context.value(this->value_.toString());
|
||||
}
|
||||
return this->value_;
|
||||
}
|
||||
|
||||
TokenType ValueExpression::type()
|
||||
{
|
||||
return this->type_;
|
||||
}
|
||||
|
||||
QString ValueExpression::debug() const
|
||||
{
|
||||
return this->value_.toString();
|
||||
}
|
||||
|
||||
QString ValueExpression::filterString() const
|
||||
{
|
||||
switch (this->type_)
|
||||
{
|
||||
case INT:
|
||||
return QString::number(this->value_.toInt());
|
||||
case STRING:
|
||||
return QString("\"%1\"").arg(
|
||||
this->value_.toString().replace("\"", "\\\""));
|
||||
case IDENTIFIER:
|
||||
return this->value_.toString();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// RegexExpression
|
||||
|
||||
RegexExpression::RegexExpression(QString regex, bool caseInsensitive)
|
||||
: regexString_(regex)
|
||||
, caseInsensitive_(caseInsensitive)
|
||||
, regex_(QRegularExpression(
|
||||
regex, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
|
||||
: QRegularExpression::NoPatternOption)){};
|
||||
|
||||
QVariant RegexExpression::execute(const ContextMap &) const
|
||||
{
|
||||
return this->regex_;
|
||||
}
|
||||
|
||||
QString RegexExpression::debug() const
|
||||
{
|
||||
return this->regexString_;
|
||||
}
|
||||
|
||||
QString RegexExpression::filterString() const
|
||||
{
|
||||
auto s = this->regexString_;
|
||||
return QString("%1\"%2\"")
|
||||
.arg(this->caseInsensitive_ ? "ri" : "r")
|
||||
.arg(s.replace("\"", "\\\""));
|
||||
}
|
||||
|
||||
// ListExpression
|
||||
|
||||
ListExpression::ListExpression(ExpressionList list)
|
||||
: list_(std::move(list)){};
|
||||
|
||||
QVariant ListExpression::execute(const ContextMap &context) const
|
||||
{
|
||||
QList<QVariant> results;
|
||||
bool allStrings = true;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
auto res = exp->execute(context);
|
||||
if (allStrings && res.type() != QVariant::Type::String)
|
||||
{
|
||||
allStrings = false;
|
||||
}
|
||||
results.append(res);
|
||||
}
|
||||
|
||||
// if everything is a string return a QStringList for case-insensitive comparison
|
||||
if (allStrings)
|
||||
{
|
||||
QStringList strings;
|
||||
strings.reserve(results.size());
|
||||
for (const auto &val : results)
|
||||
{
|
||||
strings << val.toString();
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
else
|
||||
{
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
QString ListExpression::debug() const
|
||||
{
|
||||
QStringList debugs;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
debugs.append(exp->debug());
|
||||
}
|
||||
return QString("{%1}").arg(debugs.join(", "));
|
||||
}
|
||||
|
||||
QString ListExpression::filterString() const
|
||||
{
|
||||
QStringList strings;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
strings.append(QString("(%1)").arg(exp->filterString()));
|
||||
}
|
||||
return QString("{%1}").arg(strings.join(", "));
|
||||
}
|
||||
|
||||
// BinaryOperation
|
||||
namespace chatterino::filters {
|
||||
|
||||
BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left,
|
||||
ExpressionPtr right)
|
||||
|
@ -225,7 +19,8 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
switch (this->op_)
|
||||
{
|
||||
case PLUS:
|
||||
if (left.type() == QVariant::Type::String &&
|
||||
if (static_cast<QMetaType::Type>(left.type()) ==
|
||||
QMetaType::QString &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toString().append(right.toString());
|
||||
|
@ -260,14 +55,14 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
return left.toBool() && right.toBool();
|
||||
return false;
|
||||
case EQ:
|
||||
if (variantTypesMatch(left, right, QVariant::Type::String))
|
||||
if (variantTypesMatch(left, right, QMetaType::QString))
|
||||
{
|
||||
return left.toString().compare(right.toString(),
|
||||
Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
return left == right;
|
||||
case NEQ:
|
||||
if (variantTypesMatch(left, right, QVariant::Type::String))
|
||||
if (variantTypesMatch(left, right, QMetaType::QString))
|
||||
{
|
||||
return left.toString().compare(right.toString(),
|
||||
Qt::CaseInsensitive) != 0;
|
||||
|
@ -290,20 +85,20 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
return left.toInt() >= right.toInt();
|
||||
return false;
|
||||
case CONTAINS:
|
||||
if (left.type() == QVariant::Type::StringList &&
|
||||
if (variantIs(left, QMetaType::QStringList) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toStringList().contains(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
if (left.type() == QVariant::Type::Map &&
|
||||
if (variantIs(left.type(), QMetaType::QVariantMap) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toMap().contains(right.toString());
|
||||
}
|
||||
|
||||
if (left.type() == QVariant::Type::List)
|
||||
if (variantIs(left.type(), QMetaType::QVariantList))
|
||||
{
|
||||
return left.toList().contains(right);
|
||||
}
|
||||
|
@ -317,16 +112,16 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
|
||||
return false;
|
||||
case STARTS_WITH:
|
||||
if (left.type() == QVariant::Type::StringList &&
|
||||
if (variantIs(left.type(), QMetaType::QStringList) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
auto list = left.toStringList();
|
||||
return !list.isEmpty() &&
|
||||
list.first().compare(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
if (left.type() == QVariant::Type::List)
|
||||
if (variantIs(left.type(), QMetaType::QVariantList))
|
||||
{
|
||||
return left.toList().startsWith(right);
|
||||
}
|
||||
|
@ -341,16 +136,16 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
return false;
|
||||
|
||||
case ENDS_WITH:
|
||||
if (left.type() == QVariant::Type::StringList &&
|
||||
if (variantIs(left.type(), QMetaType::QStringList) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
auto list = left.toStringList();
|
||||
return !list.isEmpty() &&
|
||||
list.last().compare(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
if (left.type() == QVariant::Type::List)
|
||||
if (variantIs(left.type(), QMetaType::QVariantList))
|
||||
{
|
||||
return left.toList().endsWith(right);
|
||||
}
|
||||
|
@ -371,14 +166,14 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
|
||||
auto matching = left.toString();
|
||||
|
||||
switch (right.type())
|
||||
switch (static_cast<QMetaType::Type>(right.type()))
|
||||
{
|
||||
case QVariant::Type::RegularExpression: {
|
||||
case QMetaType::QRegularExpression: {
|
||||
return right.toRegularExpression()
|
||||
.match(matching)
|
||||
.hasMatch();
|
||||
}
|
||||
case QVariant::Type::List: {
|
||||
case QMetaType::QVariantList: {
|
||||
auto list = right.toList();
|
||||
|
||||
// list must be two items
|
||||
|
@ -386,19 +181,19 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
return false;
|
||||
|
||||
// list must be a regular expression and an int
|
||||
if (list.at(0).type() !=
|
||||
QVariant::Type::RegularExpression ||
|
||||
list.at(1).type() != QVariant::Type::Int)
|
||||
if (variantIsNot(list.at(0),
|
||||
QMetaType::QRegularExpression) ||
|
||||
variantIsNot(list.at(1), QMetaType::Int))
|
||||
return false;
|
||||
|
||||
auto match =
|
||||
list.at(0).toRegularExpression().match(matching);
|
||||
|
||||
// if matched, return nth capture group. Otherwise, return false
|
||||
// if matched, return nth capture group. Otherwise, return ""
|
||||
if (match.hasMatch())
|
||||
return match.captured(list.at(1).toInt());
|
||||
else
|
||||
return false;
|
||||
return "";
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
|
@ -409,11 +204,105 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
|
|||
}
|
||||
}
|
||||
|
||||
QString BinaryOperation::debug() const
|
||||
PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
|
||||
{
|
||||
return QString("(%1 %2 %3)")
|
||||
.arg(this->left_->debug(), tokenTypeToInfoString(this->op_),
|
||||
this->right_->debug());
|
||||
auto leftSyn = this->left_->synthesizeType(context);
|
||||
auto rightSyn = this->right_->synthesizeType(context);
|
||||
|
||||
// Return if either operand is ill-typed
|
||||
if (isIllTyped(leftSyn))
|
||||
{
|
||||
return leftSyn;
|
||||
}
|
||||
else if (isIllTyped(rightSyn))
|
||||
{
|
||||
return rightSyn;
|
||||
}
|
||||
|
||||
auto left = std::get<TypeClass>(leftSyn);
|
||||
auto right = std::get<TypeClass>(rightSyn);
|
||||
|
||||
switch (this->op_)
|
||||
{
|
||||
case PLUS:
|
||||
if (left == Type::String)
|
||||
return TypeClass{Type::String}; // String concatenation
|
||||
else if (left == Type::Int && right == Type::Int)
|
||||
return TypeClass{Type::Int};
|
||||
|
||||
return IllTyped{this, "Can only add Ints or concatenate a String"};
|
||||
case MINUS:
|
||||
case MULTIPLY:
|
||||
case DIVIDE:
|
||||
case MOD:
|
||||
if (left == Type::Int && right == Type::Int)
|
||||
return TypeClass{Type::Int};
|
||||
|
||||
return IllTyped{this, "Can only perform operation with Ints"};
|
||||
case OR:
|
||||
case AND:
|
||||
if (left == Type::Bool && right == Type::Bool)
|
||||
return TypeClass{Type::Bool};
|
||||
|
||||
return IllTyped{this,
|
||||
"Can only perform logical operations with Bools"};
|
||||
case EQ:
|
||||
case NEQ:
|
||||
// equals/not equals always produces a valid output
|
||||
return TypeClass{Type::Bool};
|
||||
case LT:
|
||||
case GT:
|
||||
case LTE:
|
||||
case GTE:
|
||||
if (left == Type::Int && right == Type::Int)
|
||||
return TypeClass{Type::Bool};
|
||||
|
||||
return IllTyped{this, "Can only perform comparisons with Ints"};
|
||||
case STARTS_WITH:
|
||||
case ENDS_WITH:
|
||||
if (isList(left))
|
||||
return TypeClass{Type::Bool};
|
||||
if (left == Type::String && right == Type::String)
|
||||
return TypeClass{Type::Bool};
|
||||
|
||||
return IllTyped{
|
||||
this,
|
||||
"Can only perform starts/ends with a List or two Strings"};
|
||||
case CONTAINS:
|
||||
if (isList(left) || left == Type::Map)
|
||||
return TypeClass{Type::Bool};
|
||||
if (left == Type::String && right == Type::String)
|
||||
return TypeClass{Type::Bool};
|
||||
|
||||
return IllTyped{
|
||||
this,
|
||||
"Can only perform contains with a List, a Map, or two Strings"};
|
||||
case MATCH: {
|
||||
if (left != Type::String)
|
||||
return IllTyped{this,
|
||||
"Left argument of match must be a String"};
|
||||
|
||||
if (right == Type::RegularExpression)
|
||||
return TypeClass{Type::Bool};
|
||||
if (right == Type::MatchingSpecifier) // group capturing
|
||||
return TypeClass{Type::String};
|
||||
|
||||
return IllTyped{this, "Can only match on a RegularExpression or a "
|
||||
"MatchingSpecifier"};
|
||||
}
|
||||
default:
|
||||
return IllTyped{this, "Not implemented"};
|
||||
}
|
||||
}
|
||||
|
||||
QString BinaryOperation::debug(const TypingContext &context) const
|
||||
{
|
||||
return QString("BinaryOp[%1](%2 : %3, %4 : %5)")
|
||||
.arg(tokenTypeToInfoString(this->op_))
|
||||
.arg(this->left_->debug(context))
|
||||
.arg(possibleTypeToString(this->left_->synthesizeType(context)))
|
||||
.arg(this->right_->debug(context))
|
||||
.arg(possibleTypeToString(this->right_->synthesizeType(context)));
|
||||
}
|
||||
|
||||
QString BinaryOperation::filterString() const
|
||||
|
@ -456,57 +345,14 @@ QString BinaryOperation::filterString() const
|
|||
case MATCH:
|
||||
return "match";
|
||||
default:
|
||||
return QString();
|
||||
return "";
|
||||
}
|
||||
}();
|
||||
|
||||
return QString("(%1) %2 (%3)")
|
||||
return QString("(%1 %2 %3)")
|
||||
.arg(this->left_->filterString())
|
||||
.arg(opText)
|
||||
.arg(this->right_->filterString());
|
||||
}
|
||||
|
||||
// UnaryOperation
|
||||
|
||||
UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right)
|
||||
: op_(op)
|
||||
, right_(std::move(right))
|
||||
{
|
||||
}
|
||||
|
||||
QVariant UnaryOperation::execute(const ContextMap &context) const
|
||||
{
|
||||
auto right = this->right_->execute(context);
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
if (right.canConvert<bool>())
|
||||
return !right.toBool();
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString UnaryOperation::debug() const
|
||||
{
|
||||
return QString("(%1 %2)").arg(tokenTypeToInfoString(this->op_),
|
||||
this->right_->debug());
|
||||
}
|
||||
|
||||
QString UnaryOperation::filterString() const
|
||||
{
|
||||
const auto opText = [&]() -> QString {
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
return "!";
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}();
|
||||
|
||||
return QString("%1(%2)").arg(opText).arg(this->right_->filterString());
|
||||
}
|
||||
|
||||
} // namespace filterparser
|
||||
} // namespace chatterino::filters
|
24
src/controllers/filters/lang/expressions/BinaryOperation.hpp
Normal file
24
src/controllers/filters/lang/expressions/BinaryOperation.hpp
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class BinaryOperation : public Expression
|
||||
{
|
||||
public:
|
||||
BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||
QString debug(const TypingContext &context) const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
TokenType op_;
|
||||
ExpressionPtr left_;
|
||||
ExpressionPtr right_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::filters
|
25
src/controllers/filters/lang/expressions/Expression.cpp
Normal file
25
src/controllers/filters/lang/expressions/Expression.cpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
QVariant Expression::execute(const ContextMap & /*context*/) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PossibleType Expression::synthesizeType(const TypingContext & /*context*/) const
|
||||
{
|
||||
return IllTyped{this, "Not implemented"};
|
||||
}
|
||||
|
||||
QString Expression::debug(const TypingContext & /*context*/) const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
QString Expression::filterString() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
28
src/controllers/filters/lang/expressions/Expression.hpp
Normal file
28
src/controllers/filters/lang/expressions/Expression.hpp
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class Expression
|
||||
{
|
||||
public:
|
||||
virtual ~Expression() = default;
|
||||
|
||||
virtual QVariant execute(const ContextMap &context) const;
|
||||
virtual PossibleType synthesizeType(const TypingContext &context) const;
|
||||
virtual QString debug(const TypingContext &context) const;
|
||||
virtual QString filterString() const;
|
||||
};
|
||||
|
||||
using ExpressionPtr = std::unique_ptr<Expression>;
|
||||
using ExpressionList = std::vector<std::unique_ptr<Expression>>;
|
||||
|
||||
} // namespace chatterino::filters
|
94
src/controllers/filters/lang/expressions/ListExpression.cpp
Normal file
94
src/controllers/filters/lang/expressions/ListExpression.cpp
Normal file
|
@ -0,0 +1,94 @@
|
|||
#include "controllers/filters/lang/expressions/ListExpression.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
ListExpression::ListExpression(ExpressionList &&list)
|
||||
: list_(std::move(list)){};
|
||||
|
||||
QVariant ListExpression::execute(const ContextMap &context) const
|
||||
{
|
||||
QList<QVariant> results;
|
||||
bool allStrings = true;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
auto res = exp->execute(context);
|
||||
if (allStrings && variantIsNot(res.type(), QMetaType::QString))
|
||||
{
|
||||
allStrings = false;
|
||||
}
|
||||
results.append(res);
|
||||
}
|
||||
|
||||
// if everything is a string return a QStringList for case-insensitive comparison
|
||||
if (allStrings)
|
||||
{
|
||||
QStringList strings;
|
||||
strings.reserve(results.size());
|
||||
for (const auto &val : results)
|
||||
{
|
||||
strings << val.toString();
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
PossibleType ListExpression::synthesizeType(const TypingContext &context) const
|
||||
{
|
||||
std::vector<TypeClass> types;
|
||||
types.reserve(this->list_.size());
|
||||
bool allStrings = true;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
auto typSyn = exp->synthesizeType(context);
|
||||
if (isIllTyped(typSyn))
|
||||
{
|
||||
return typSyn; // Ill-typed
|
||||
}
|
||||
|
||||
auto typ = std::get<TypeClass>(typSyn);
|
||||
|
||||
if (typ != Type::String)
|
||||
{
|
||||
allStrings = false;
|
||||
}
|
||||
|
||||
types.push_back(typ);
|
||||
}
|
||||
|
||||
if (types.size() == 2 && types[0] == Type::RegularExpression &&
|
||||
types[1] == Type::Int)
|
||||
{
|
||||
// Specific {RegularExpression, Int} form
|
||||
return TypeClass{Type::MatchingSpecifier};
|
||||
}
|
||||
|
||||
return allStrings ? TypeClass{Type::StringList} : TypeClass{Type::List};
|
||||
}
|
||||
|
||||
QString ListExpression::debug(const TypingContext &context) const
|
||||
{
|
||||
QStringList debugs;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
debugs.append(
|
||||
QString("%1 : %2")
|
||||
.arg(exp->debug(context))
|
||||
.arg(possibleTypeToString(exp->synthesizeType(context))));
|
||||
}
|
||||
|
||||
return QString("List(%1)").arg(debugs.join(", "));
|
||||
}
|
||||
|
||||
QString ListExpression::filterString() const
|
||||
{
|
||||
QStringList strings;
|
||||
for (const auto &exp : this->list_)
|
||||
{
|
||||
strings.append(exp->filterString());
|
||||
}
|
||||
return QString("{%1}").arg(strings.join(", "));
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
22
src/controllers/filters/lang/expressions/ListExpression.hpp
Normal file
22
src/controllers/filters/lang/expressions/ListExpression.hpp
Normal file
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class ListExpression : public Expression
|
||||
{
|
||||
public:
|
||||
ListExpression(ExpressionList &&list);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||
QString debug(const TypingContext &context) const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
ExpressionList list_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::filters
|
36
src/controllers/filters/lang/expressions/RegexExpression.cpp
Normal file
36
src/controllers/filters/lang/expressions/RegexExpression.cpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#include "controllers/filters/lang/expressions/RegexExpression.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
RegexExpression::RegexExpression(const QString ®ex, bool caseInsensitive)
|
||||
: regexString_(regex)
|
||||
, caseInsensitive_(caseInsensitive)
|
||||
, regex_(QRegularExpression(
|
||||
regex, caseInsensitive ? QRegularExpression::CaseInsensitiveOption
|
||||
: QRegularExpression::NoPatternOption)){};
|
||||
|
||||
QVariant RegexExpression::execute(const ContextMap & /*context*/) const
|
||||
{
|
||||
return this->regex_;
|
||||
}
|
||||
|
||||
PossibleType RegexExpression::synthesizeType(
|
||||
const TypingContext & /*context*/) const
|
||||
{
|
||||
return TypeClass{Type::RegularExpression};
|
||||
}
|
||||
|
||||
QString RegexExpression::debug(const TypingContext & /*context*/) const
|
||||
{
|
||||
return QString("RegEx(%1)").arg(this->regexString_);
|
||||
}
|
||||
|
||||
QString RegexExpression::filterString() const
|
||||
{
|
||||
auto s = this->regexString_;
|
||||
return QString("%1\"%2\"")
|
||||
.arg(this->caseInsensitive_ ? "ri" : "r")
|
||||
.arg(s.replace("\"", "\\\""));
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
26
src/controllers/filters/lang/expressions/RegexExpression.hpp
Normal file
26
src/controllers/filters/lang/expressions/RegexExpression.hpp
Normal file
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class RegexExpression : public Expression
|
||||
{
|
||||
public:
|
||||
RegexExpression(const QString ®ex, bool caseInsensitive);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||
QString debug(const TypingContext &context) const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
QString regexString_;
|
||||
bool caseInsensitive_;
|
||||
QRegularExpression regex_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::filters
|
69
src/controllers/filters/lang/expressions/UnaryOperation.cpp
Normal file
69
src/controllers/filters/lang/expressions/UnaryOperation.cpp
Normal file
|
@ -0,0 +1,69 @@
|
|||
#include "controllers/filters/lang/expressions/UnaryOperation.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right)
|
||||
: op_(op)
|
||||
, right_(std::move(right))
|
||||
{
|
||||
}
|
||||
|
||||
QVariant UnaryOperation::execute(const ContextMap &context) const
|
||||
{
|
||||
auto right = this->right_->execute(context);
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
return right.canConvert<bool>() && !right.toBool();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PossibleType UnaryOperation::synthesizeType(const TypingContext &context) const
|
||||
{
|
||||
auto rightSyn = this->right_->synthesizeType(context);
|
||||
if (isIllTyped(rightSyn))
|
||||
{
|
||||
return rightSyn;
|
||||
}
|
||||
|
||||
auto right = std::get<TypeClass>(rightSyn);
|
||||
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
if (right == Type::Bool)
|
||||
{
|
||||
return TypeClass{Type::Bool};
|
||||
}
|
||||
return IllTyped{this, "Can only negate boolean values"};
|
||||
default:
|
||||
return IllTyped{this, "Not implemented"};
|
||||
}
|
||||
}
|
||||
|
||||
QString UnaryOperation::debug(const TypingContext &context) const
|
||||
{
|
||||
return QString("UnaryOp[%1](%2 : %3)")
|
||||
.arg(tokenTypeToInfoString(this->op_))
|
||||
.arg(this->right_->debug(context))
|
||||
.arg(possibleTypeToString(this->right_->synthesizeType(context)));
|
||||
}
|
||||
|
||||
QString UnaryOperation::filterString() const
|
||||
{
|
||||
const auto opText = [&]() -> QString {
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
return "!";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}();
|
||||
|
||||
return QString("(%1%2)").arg(opText).arg(this->right_->filterString());
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
23
src/controllers/filters/lang/expressions/UnaryOperation.hpp
Normal file
23
src/controllers/filters/lang/expressions/UnaryOperation.hpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class UnaryOperation : public Expression
|
||||
{
|
||||
public:
|
||||
UnaryOperation(TokenType op, ExpressionPtr right);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||
QString debug(const TypingContext &context) const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
TokenType op_;
|
||||
ExpressionPtr right_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::filters
|
70
src/controllers/filters/lang/expressions/ValueExpression.cpp
Normal file
70
src/controllers/filters/lang/expressions/ValueExpression.cpp
Normal file
|
@ -0,0 +1,70 @@
|
|||
#include "controllers/filters/lang/expressions/ValueExpression.hpp"
|
||||
|
||||
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
ValueExpression::ValueExpression(QVariant value, TokenType type)
|
||||
: value_(std::move(value))
|
||||
, type_(type)
|
||||
{
|
||||
}
|
||||
|
||||
QVariant ValueExpression::execute(const ContextMap &context) const
|
||||
{
|
||||
if (this->type_ == TokenType::IDENTIFIER)
|
||||
{
|
||||
return context.value(this->value_.toString());
|
||||
}
|
||||
return this->value_;
|
||||
}
|
||||
|
||||
PossibleType ValueExpression::synthesizeType(const TypingContext &context) const
|
||||
{
|
||||
switch (this->type_)
|
||||
{
|
||||
case TokenType::IDENTIFIER: {
|
||||
auto it = context.find(this->value_.toString());
|
||||
if (it != context.end())
|
||||
{
|
||||
return TypeClass{it.value()};
|
||||
}
|
||||
|
||||
return IllTyped{this, "Unbound identifier"};
|
||||
}
|
||||
case TokenType::INT:
|
||||
return TypeClass{Type::Int};
|
||||
case TokenType::STRING:
|
||||
return TypeClass{Type::String};
|
||||
default:
|
||||
return IllTyped{this, "Invalid value type"};
|
||||
}
|
||||
}
|
||||
|
||||
TokenType ValueExpression::type()
|
||||
{
|
||||
return this->type_;
|
||||
}
|
||||
|
||||
QString ValueExpression::debug(const TypingContext & /*context*/) const
|
||||
{
|
||||
return QString("Val(%1)").arg(this->value_.toString());
|
||||
}
|
||||
|
||||
QString ValueExpression::filterString() const
|
||||
{
|
||||
switch (this->type_)
|
||||
{
|
||||
case INT:
|
||||
return QString::number(this->value_.toInt());
|
||||
case STRING:
|
||||
return QString("\"%1\"").arg(
|
||||
this->value_.toString().replace("\"", "\\\""));
|
||||
case IDENTIFIER:
|
||||
return this->value_.toString();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
24
src/controllers/filters/lang/expressions/ValueExpression.hpp
Normal file
24
src/controllers/filters/lang/expressions/ValueExpression.hpp
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/filters/lang/expressions/Expression.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
class ValueExpression : public Expression
|
||||
{
|
||||
public:
|
||||
ValueExpression(QVariant value, TokenType type);
|
||||
TokenType type();
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
PossibleType synthesizeType(const TypingContext &context) const override;
|
||||
QString debug(const TypingContext &context) const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
QVariant value_;
|
||||
TokenType type_;
|
||||
};
|
||||
|
||||
} // namespace chatterino::filters
|
|
@ -1,168 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct Message;
|
||||
|
||||
}
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
using MessagePtr = std::shared_ptr<const chatterino::Message>;
|
||||
using ContextMap = QMap<QString, QVariant>;
|
||||
|
||||
enum TokenType {
|
||||
// control
|
||||
CONTROL_START = 0,
|
||||
AND = 1,
|
||||
OR = 2,
|
||||
LP = 3,
|
||||
RP = 4,
|
||||
LIST_START = 5,
|
||||
LIST_END = 6,
|
||||
COMMA = 7,
|
||||
CONTROL_END = 19,
|
||||
|
||||
// binary operator
|
||||
BINARY_START = 20,
|
||||
EQ = 21,
|
||||
NEQ = 22,
|
||||
LT = 23,
|
||||
GT = 24,
|
||||
LTE = 25,
|
||||
GTE = 26,
|
||||
CONTAINS = 27,
|
||||
STARTS_WITH = 28,
|
||||
ENDS_WITH = 29,
|
||||
MATCH = 30,
|
||||
BINARY_END = 49,
|
||||
|
||||
// unary operator
|
||||
UNARY_START = 50,
|
||||
NOT = 51,
|
||||
UNARY_END = 99,
|
||||
|
||||
// math operators
|
||||
MATH_START = 100,
|
||||
PLUS = 101,
|
||||
MINUS = 102,
|
||||
MULTIPLY = 103,
|
||||
DIVIDE = 104,
|
||||
MOD = 105,
|
||||
MATH_END = 149,
|
||||
|
||||
// other types
|
||||
OTHER_START = 150,
|
||||
STRING = 151,
|
||||
INT = 152,
|
||||
IDENTIFIER = 153,
|
||||
REGULAR_EXPRESSION = 154,
|
||||
|
||||
NONE = 200
|
||||
};
|
||||
|
||||
bool convertVariantTypes(QVariant &a, QVariant &b, int type);
|
||||
QString tokenTypeToInfoString(TokenType type);
|
||||
|
||||
class Expression
|
||||
{
|
||||
public:
|
||||
virtual ~Expression() = default;
|
||||
|
||||
virtual QVariant execute(const ContextMap &) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual QString debug() const
|
||||
{
|
||||
return "(false)";
|
||||
}
|
||||
|
||||
virtual QString filterString() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
using ExpressionPtr = std::unique_ptr<Expression>;
|
||||
|
||||
class ValueExpression : public Expression
|
||||
{
|
||||
public:
|
||||
ValueExpression(QVariant value, TokenType type);
|
||||
TokenType type();
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
QVariant value_;
|
||||
TokenType type_;
|
||||
};
|
||||
|
||||
class RegexExpression : public Expression
|
||||
{
|
||||
public:
|
||||
RegexExpression(QString regex, bool caseInsensitive);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
QString regexString_;
|
||||
bool caseInsensitive_;
|
||||
QRegularExpression regex_;
|
||||
};
|
||||
|
||||
using ExpressionList = std::vector<std::unique_ptr<Expression>>;
|
||||
|
||||
class ListExpression : public Expression
|
||||
{
|
||||
public:
|
||||
ListExpression(ExpressionList list);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
ExpressionList list_;
|
||||
};
|
||||
|
||||
class BinaryOperation : public Expression
|
||||
{
|
||||
public:
|
||||
BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
TokenType op_;
|
||||
ExpressionPtr left_;
|
||||
ExpressionPtr right_;
|
||||
};
|
||||
|
||||
class UnaryOperation : public Expression
|
||||
{
|
||||
public:
|
||||
UnaryOperation(TokenType op, ExpressionPtr right);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
TokenType op_;
|
||||
ExpressionPtr right_;
|
||||
};
|
||||
|
||||
} // namespace filterparser
|
|
@ -1,6 +1,6 @@
|
|||
#include "ChannelFilterEditorDialog.hpp"
|
||||
#include "widgets/dialogs/ChannelFilterEditorDialog.hpp"
|
||||
|
||||
#include "controllers/filters/parser/FilterParser.hpp"
|
||||
#include "controllers/filters/lang/Tokenizer.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
|
@ -99,7 +99,8 @@ ChannelFilterEditorDialog::ValueSpecifier::ValueSpecifier()
|
|||
|
||||
this->typeCombo_->insertItems(
|
||||
0, {"Constant Text", "Constant Number", "Variable"});
|
||||
this->varCombo_->insertItems(0, filterparser::validIdentifiersMap.values());
|
||||
|
||||
this->varCombo_->insertItems(0, filters::validIdentifiersMap.values());
|
||||
|
||||
this->layout_->addWidget(this->typeCombo_);
|
||||
this->layout_->addWidget(this->varCombo_, 1);
|
||||
|
@ -141,7 +142,7 @@ void ChannelFilterEditorDialog::ValueSpecifier::setValue(const QString &value)
|
|||
if (this->typeCombo_->currentIndex() == 2)
|
||||
{
|
||||
this->varCombo_->setCurrentText(
|
||||
filterparser::validIdentifiersMap.value(value));
|
||||
filters::validIdentifiersMap.value(value));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -164,7 +165,7 @@ QString ChannelFilterEditorDialog::ValueSpecifier::expressionText()
|
|||
case 1: // number
|
||||
return this->valueInput_->text();
|
||||
case 2: // variable
|
||||
return filterparser::validIdentifiersMap.key(
|
||||
return filters::validIdentifiersMap.key(
|
||||
this->varCombo_->currentText());
|
||||
default:
|
||||
return "";
|
||||
|
@ -221,7 +222,7 @@ QString ChannelFilterEditorDialog::BinaryOperationSpecifier::expressionText()
|
|||
return this->left_->expressionText();
|
||||
}
|
||||
|
||||
return QString("(%1) %2 (%3)")
|
||||
return QString("(%1 %2 %3)")
|
||||
.arg(this->left_->expressionText())
|
||||
.arg(opText)
|
||||
.arg(this->right_->expressionText());
|
||||
|
|
|
@ -91,10 +91,14 @@ void FiltersPage::tableCellClicked(const QModelIndex &clicked,
|
|||
{
|
||||
QMessageBox popup(this->window());
|
||||
|
||||
filterparser::FilterParser f(
|
||||
view->getModel()->data(clicked.siblingAtColumn(1)).toString());
|
||||
auto filterText =
|
||||
view->getModel()->data(clicked.siblingAtColumn(1)).toString();
|
||||
auto filterResult = filters::Filter::fromString(filterText);
|
||||
|
||||
if (f.valid())
|
||||
if (std::holds_alternative<filters::Filter>(filterResult))
|
||||
{
|
||||
auto f = std::move(std::get<filters::Filter>(filterResult));
|
||||
if (f.returnType() == filters::Type::Bool)
|
||||
{
|
||||
popup.setIcon(QMessageBox::Icon::Information);
|
||||
popup.setWindowTitle("Valid filter");
|
||||
|
@ -104,10 +108,22 @@ void FiltersPage::tableCellClicked(const QModelIndex &clicked,
|
|||
}
|
||||
else
|
||||
{
|
||||
popup.setIcon(QMessageBox::Icon::Warning);
|
||||
popup.setWindowTitle("Invalid filter");
|
||||
popup.setText(QString("Unexpected filter return type"));
|
||||
popup.setInformativeText(
|
||||
QString("Expected %1 but got %2")
|
||||
.arg(filters::typeToString(filters::Type::Bool))
|
||||
.arg(filters::typeToString(f.returnType())));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto err = std::move(std::get<filters::FilterError>(filterResult));
|
||||
popup.setIcon(QMessageBox::Icon::Warning);
|
||||
popup.setWindowTitle("Invalid filter");
|
||||
popup.setText(QString("Parsing errors occurred:"));
|
||||
popup.setInformativeText(f.errors().join("\n"));
|
||||
popup.setInformativeText(err.message);
|
||||
}
|
||||
|
||||
popup.exec();
|
||||
|
|
|
@ -24,6 +24,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/BttvLiveUpdates.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Updates.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Filters.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
|
||||
# Add your new file above this line!
|
||||
)
|
||||
|
|
172
tests/src/Filters.cpp
Normal file
172
tests/src/Filters.cpp
Normal file
|
@ -0,0 +1,172 @@
|
|||
#include "controllers/filters/lang/Filter.hpp"
|
||||
#include "controllers/filters/lang/Types.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QColor>
|
||||
#include <QVariant>
|
||||
|
||||
using namespace chatterino;
|
||||
using namespace chatterino::filters;
|
||||
|
||||
TypingContext typingContext = MESSAGE_TYPING_CONTEXT;
|
||||
|
||||
namespace chatterino::filters {
|
||||
|
||||
std::ostream &operator<<(std::ostream &os, Type t)
|
||||
{
|
||||
os << qUtf8Printable(typeToString(t));
|
||||
return os;
|
||||
}
|
||||
|
||||
} // namespace chatterino::filters
|
||||
|
||||
TEST(Filters, Validity)
|
||||
{
|
||||
struct TestCase {
|
||||
QString input;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
std::vector<TestCase> tests{
|
||||
{"", false},
|
||||
{R".(1 + 1).", true},
|
||||
{R".(1 + ).", false},
|
||||
{R".(1 + 1)).", false},
|
||||
{R".((1 + 1).", false},
|
||||
{R".(author.name contains "icelys").", true},
|
||||
{R".(author.color == "#ff0000").", true},
|
||||
{R".(author.name - 5).", false}, // can't perform String - Int
|
||||
{R".(message.content match {r"(\d\d)/(\d\d)/(\d\d\d\d)", 3}).", true},
|
||||
{R".("abc" + 123 == "abc123").", true},
|
||||
{R".(123 + "abc" == "hello").", false},
|
||||
{R".(flags.reply && flags.automod).", true},
|
||||
{R".(unknown.identifier).", false},
|
||||
{R".(channel.name == "forsen" && author.badges contains "moderator").", true},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
for (const auto &[input, expected] : tests)
|
||||
{
|
||||
auto filterResult = Filter::fromString(input);
|
||||
bool isValid = std::holds_alternative<Filter>(filterResult);
|
||||
EXPECT_EQ(isValid, expected)
|
||||
<< "Filter::fromString( " << qUtf8Printable(input)
|
||||
<< " ) should be " << (expected ? "valid" : "invalid");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Filters, TypeSynthesis)
|
||||
{
|
||||
using T = Type;
|
||||
struct TestCase {
|
||||
QString input;
|
||||
T type;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
std::vector<TestCase> tests
|
||||
{
|
||||
{R".(1 + 1).", T::Int},
|
||||
{R".(author.color).", T::Color},
|
||||
{R".(author.name).", T::String},
|
||||
{R".(!author.subbed).", T::Bool},
|
||||
{R".(author.badges).", T::StringList},
|
||||
{R".(channel.name == "forsen" && author.badges contains "moderator").", T::Bool},
|
||||
{R".(message.content match {r"(\d\d)/(\d\d)/(\d\d\d\d)", 3}).", T::String},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
for (const auto &[input, expected] : tests)
|
||||
{
|
||||
auto filterResult = Filter::fromString(input);
|
||||
bool isValid = std::holds_alternative<Filter>(filterResult);
|
||||
ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input)
|
||||
<< " ) is invalid";
|
||||
|
||||
auto filter = std::move(std::get<Filter>(filterResult));
|
||||
T type = filter.returnType();
|
||||
EXPECT_EQ(type, expected)
|
||||
<< "Filter{ " << qUtf8Printable(input) << " } has type " << type
|
||||
<< " instead of " << expected << ".\nDebug: "
|
||||
<< qUtf8Printable(filter.debugString(typingContext));
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Filters, Evaluation)
|
||||
{
|
||||
struct TestCase {
|
||||
QString input;
|
||||
QVariant output;
|
||||
};
|
||||
|
||||
ContextMap contextMap = {
|
||||
{"author.name", QVariant("icelys")},
|
||||
{"author.color", QVariant(QColor("#ff0000"))},
|
||||
{"author.subbed", QVariant(false)},
|
||||
{"message.content", QVariant("hey there :) 2038-01-19 123 456")},
|
||||
{"channel.name", QVariant("forsen")},
|
||||
{"author.badges", QVariant(QStringList({"moderator", "staff"}))}};
|
||||
|
||||
// clang-format off
|
||||
std::vector<TestCase> tests
|
||||
{
|
||||
// Evaluation semantics
|
||||
{R".(1 + 1).", QVariant(2)},
|
||||
{R".(!(1 == 1)).", QVariant(false)},
|
||||
{R".(2 + 3 * 4).", QVariant(20)}, // math operators have the same precedence
|
||||
{R".(1 > 2 || 3 >= 3).", QVariant(true)},
|
||||
{R".(1 > 2 && 3 > 1).", QVariant(false)},
|
||||
{R".("abc" + 123).", QVariant("abc123")},
|
||||
{R".("abc" + "456").", QVariant("abc456")},
|
||||
{R".(3 - 4).", QVariant(-1)},
|
||||
{R".(3 * 4).", QVariant(12)},
|
||||
{R".(8 / 3).", QVariant(2)},
|
||||
{R".(7 % 3).", QVariant(1)},
|
||||
{R".(5 == 5).", QVariant(true)},
|
||||
{R".(5 == "5").", QVariant(true)},
|
||||
{R".(5 != 7).", QVariant(true)},
|
||||
{R".(5 == "abc").", QVariant(false)},
|
||||
{R".("ABC123" == "abc123").", QVariant(true)}, // String comparison is case-insensitive
|
||||
{R".("Hello world" contains "Hello").", QVariant(true)},
|
||||
{R".("Hello world" contains "LLO W").", QVariant(true)}, // Case-insensitive
|
||||
{R".({"abc", "def"} contains "abc").", QVariant(true)},
|
||||
{R".({"abc", "def"} contains "ABC").", QVariant(true)}, // Case-insensitive when list is all strings
|
||||
{R".({123, "def"} contains "DEF").", QVariant(false)}, // Case-sensitive if list not all strings
|
||||
{R".({"a123", "b456"} startswith "a123").", QVariant(true)},
|
||||
{R".({"a123", "b456"} startswith "A123").", QVariant(true)},
|
||||
{R".({} startswith "A123").", QVariant(false)},
|
||||
{R".("Hello world" startswith "Hello").", QVariant(true)},
|
||||
{R".("Hello world" startswith "world").", QVariant(false)},
|
||||
{R".({"a123", "b456"} endswith "b456").", QVariant(true)},
|
||||
{R".({"a123", "b456"} endswith "B456").", QVariant(true)},
|
||||
{R".("Hello world" endswith "world").", QVariant(true)},
|
||||
{R".("Hello world" endswith "Hello").", QVariant(false)},
|
||||
// Context map usage
|
||||
{R".(author.name).", QVariant("icelys")},
|
||||
{R".(!author.subbed).", QVariant(true)},
|
||||
{R".(author.color == "#ff0000").", QVariant(true)},
|
||||
{R".(channel.name == "forsen" && author.badges contains "moderator").", QVariant(true)},
|
||||
{R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", QVariant("19")},
|
||||
{R".(message.content match r"HEY THERE").", QVariant(false)},
|
||||
{R".(message.content match ri"HEY THERE").", QVariant(true)},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
for (const auto &[input, expected] : tests)
|
||||
{
|
||||
auto filterResult = Filter::fromString(input);
|
||||
bool isValid = std::holds_alternative<Filter>(filterResult);
|
||||
ASSERT_TRUE(isValid) << "Filter::fromString( " << qUtf8Printable(input)
|
||||
<< " ) is invalid";
|
||||
|
||||
auto filter = std::move(std::get<Filter>(filterResult));
|
||||
auto result = filter.execute(contextMap);
|
||||
|
||||
EXPECT_EQ(result, expected)
|
||||
<< "Filter{ " << qUtf8Printable(input) << " } evaluated to "
|
||||
<< qUtf8Printable(result.toString()) << " instead of "
|
||||
<< qUtf8Printable(expected.toString()) << ".\nDebug: "
|
||||
<< qUtf8Printable(filter.debugString(typingContext));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue