Introduce QAbstractItemModel::checkIndex()

When implementing a custom model there's the habit, in each and every
function that takes a QModelIndex, to carefully checking the index
passed by the caller. This index is checked for "legality" (*): does the
index belong to this model, is the index pointing to an existing row and
column, and so on.  These checks are hand-rolled and, as such, slightly
different and possibly incomplete (i.e. wrong) every time.

What's worse, these checks are implemented via "ordinary" code (if
statements). However, passing an illegal index to a QAIM function is a
precondition violation, and as such does not (and must not) be
checked in ordinary conditions, as it triggers undefined behavior. On
the other hand, while debugging a custom model or a custom hierarchy
of (proxy) models, having such checks in place can be a significant
aid.

Enter checkIndex(): a debugging helper for QAbstractItemModel and its
subclasses. checkIndex() centralizes the checks for legality of a
given index. User code is free to assert on it, or have some other
fallback mechanism in case a check fails.

(*) Using "legality" here instead of "validity" in order to avoid
confusion between QModelIndex::isValid() and what checkIndex() really
does.

[ChangeLog][QtCore][QAbstractItemModel] Added
QAbstractItemModel::checkIndex(), a debugging function for
QAbstractItemModel subclasses.

Change-Id: I1eea0586b1ac3ededdbfbf46759145022dc5ad86
Reviewed-by: Thorbjørn Lund Martsum <tmartsum@gmail.com>
Reviewed-by: David Faure <david.faure@kdab.com>
This commit is contained in:
Giuseppe D'Angelo 2017-09-01 18:27:15 +02:00
parent 9c53f4d33a
commit 69496d4e22
4 changed files with 223 additions and 1 deletions

View File

@ -48,11 +48,14 @@
#include <qstack.h>
#include <qbitarray.h>
#include <qdatetime.h>
#include <qloggingcategory.h>
#include <limits.h>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcCheckIndex, "qt.core.qabstractitemmodel.checkindex")
QPersistentModelIndexData *QPersistentModelIndexData::create(const QModelIndex &index)
{
Q_ASSERT(index.isValid()); // we will _never_ insert an invalid index in the list
@ -3320,6 +3323,140 @@ QModelIndexList QAbstractItemModel::persistentIndexList() const
return result;
}
/*!
\enum QAbstractItemModel::CheckIndexOption
\since 5.11
This enum can be used to control the checks performed by
QAbstractItemModel::checkIndex().
\value CheckIndexOption::NoOption No check options are specified.
\value CheckIndexOption::IndexIsValid The model index passed to
QAbstractItemModel::checkIndex() is checked to be a valid model index.
\value CheckIndexOption::DoNotUseParent Does not perform any check
involving the usage of the parent of the index passed to
QAbstractItemModel::checkIndex().
\value CheckIndexOption::ParentIsInvalid The parent of the model index
passed to QAbstractItemModel::checkIndex() is checked to be an invalid
model index. If both this option and CheckIndexOption::DoNotUseParent
are specified, then this option is ignored.
*/
/*!
\since 5.11
This function checks whether \a index is a legal model index for
this model. A legal model index is either an invalid model index, or a
valid model index for which all the following holds:
\list
\li the index' model is \c{this};
\li the index' row is greater or equal than zero;
\li the index' row is less than the row count for the index' parent;
\li the index' column is greater or equal than zero;
\li the index' column is less than the column count for the index' parent.
\endlist
The \a options argument may change some of these checks. If \a options
contains \c{CheckIndexOption::IndexIsValid}, then \a index must be a valid
index; this is useful when reimplementing functions such as \l{data()} or
\l{setData()}, which expect valid indexes.
If \a options contains \c{CheckIndexOption::DoNotUseParent}, then the
checks that would call \l{parent()} are omitted; this allows calling this
function from a \l{parent()} reimplementation (otherwise, this would result
in endless recursion and a crash).
If \a options does not contain \c{CheckIndexOption::DoNotUseParent}, and it
contains \c{CheckIndexOption::ParentIsInvalid}, then an additional check is
performed: the parent index is checked for not being valid. This is useful
when implementing flat models such as lists or tables, where no model index
should have a valid parent index.
This function returns true if all the checks succeeded, and false otherwise.
This allows to use the function in \l{Q_ASSERT} and similar other debugging
mechanisms. If some check failed, a warning message will be printed in the
\c{qt.core.qabstractitemmodel.checkindex} logging category, containing
some information that may be useful for debugging the failure.
\note This function is a debugging helper for implementing your own item
models. When developing complex models, as well as when building
complicated model hierarchies (e.g. using proxy models), it is useful to
call this function in order to catch bugs relative to illegal model indices
(as defined above) accidentally passed to some QAbstractItemModel API.
\warning Note that it's undefined behavior to pass illegal indices to item
models, so applications must refrain from doing so, and not rely on any
"defensive" programming that item models could employ to handle illegal
indexes gracefully.
\sa QModelIndex
*/
bool QAbstractItemModel::checkIndex(const QModelIndex &index, CheckIndexOptions options) const
{
if (!index.isValid()) {
if (options & CheckIndexOption::IndexIsValid) {
qCWarning(lcCheckIndex) << "Index" << index << "is not valid (expected valid)";
return false;
}
return true;
}
if (index.model() != this) {
qCWarning(lcCheckIndex) << "Index" << index
<< "is for model" << index.model()
<< "which is different from this model" << this;
return false;
}
if (index.row() < 0) {
qCWarning(lcCheckIndex) << "Index" << index
<< "has negative row" << index.row();
return false;
}
if (index.column() < 0) {
qCWarning(lcCheckIndex) << "Index" << index
<< "has negative column" << index.column();
return false;
}
if (!(options & CheckIndexOption::DoNotUseParent)) {
const QModelIndex parentIndex = index.parent();
if (options & CheckIndexOption::ParentIsInvalid) {
if (parentIndex.isValid()) {
qCWarning(lcCheckIndex) << "Index" << index
<< "has valid parent" << parentIndex
<< "(expected an invalid parent)";
return false;
}
}
const int rc = rowCount(parentIndex);
if (index.row() >= rc) {
qCWarning(lcCheckIndex) << "Index" << index
<< "has out of range row" << index.row()
<< "rowCount() is" << rc;
return false;
}
const int cc = columnCount(parentIndex);
if (index.column() >= cc) {
qCWarning(lcCheckIndex) << "Index" << index
<< "has out of range column" << index.column()
<< "columnCount() is" << cc;
return false;
}
}
return true;
}
/*!
\class QAbstractTableModel

View File

@ -250,6 +250,17 @@ public:
};
Q_ENUM(LayoutChangeHint)
enum class CheckIndexOption {
NoOption = 0x0000,
IndexIsValid = 0x0001,
DoNotUseParent = 0x0002,
ParentIsInvalid = 0x0004,
};
Q_ENUM(CheckIndexOption)
Q_DECLARE_FLAGS(CheckIndexOptions, CheckIndexOption)
Q_REQUIRED_RESULT bool checkIndex(const QModelIndex &index, CheckIndexOptions options = CheckIndexOption::NoOption) const;
Q_SIGNALS:
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>());
void headerDataChanged(Qt::Orientation orientation, int first, int last);
@ -343,6 +354,8 @@ private:
Q_DISABLE_COPY(QAbstractItemModel)
};
Q_DECLARE_OPERATORS_FOR_FLAGS(QAbstractItemModel::CheckIndexOptions)
inline bool QAbstractItemModel::insertRow(int arow, const QModelIndex &aparent)
{ return insertRows(arow, 1, aparent); }
inline bool QAbstractItemModel::insertColumn(int acolumn, const QModelIndex &aparent)

View File

@ -1,6 +1,6 @@
CONFIG += testcase
TARGET = tst_qabstractitemmodel
QT = core testlib
QT = core testlib gui
mtdir = ../../../other/modeltest
INCLUDEPATH += $$PWD/$${mtdir}

View File

@ -31,6 +31,7 @@
#include <QtCore/QSortFilterProxyModel>
#include <QtCore/QStringListModel>
#include <QtGui/QStandardItemModel>
#include "dynamictreemodel.h"
@ -105,6 +106,8 @@ private slots:
void testFunctionPointerSignalConnection();
void checkIndex();
private:
DynamicTreeModel *m_model;
};
@ -2283,6 +2286,75 @@ void tst_QAbstractItemModel::testFunctionPointerSignalConnection()
// model.rowsInserted(QModelIndex(), 0, 0);
}
void tst_QAbstractItemModel::checkIndex()
{
const QRegularExpression ignorePattern("^Index QModelIndex");
// checkIndex is QAbstractItemModel API; using QStandardItem as an easy
// way to build a tree model
QStandardItemModel model;
QStandardItem *topLevel = new QStandardItem("topLevel");
model.appendRow(topLevel);
topLevel->appendRow(new QStandardItem("child1"));
topLevel->appendRow(new QStandardItem("child2"));
QVERIFY(model.checkIndex(QModelIndex()));
QVERIFY(model.checkIndex(QModelIndex(), QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QVERIFY(model.checkIndex(QModelIndex(), QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(QModelIndex(), QAbstractItemModel::CheckIndexOption::IndexIsValid));
QModelIndex topLevelIndex = model.index(0, 0);
QVERIFY(topLevelIndex.isValid());
QVERIFY(model.checkIndex(topLevelIndex));
QVERIFY(model.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QVERIFY(model.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QVERIFY(model.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid));
QModelIndex childIndex = model.index(0, 0, topLevelIndex);
QVERIFY(childIndex.isValid());
QVERIFY(model.checkIndex(childIndex));
QVERIFY(model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QVERIFY(model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid));
childIndex = model.index(1, 0, topLevelIndex);
QVERIFY(childIndex.isValid());
QVERIFY(model.checkIndex(childIndex));
QVERIFY(model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QVERIFY(model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid));
topLevel->removeRow(1);
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(childIndex));
QVERIFY(model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(childIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid));
QStandardItemModel model2;
model2.appendRow(new QStandardItem("otherTopLevel"));
topLevelIndex = model2.index(0, 0);
QVERIFY(topLevelIndex.isValid());
QVERIFY(model2.checkIndex(topLevelIndex));
QVERIFY(model2.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QVERIFY(model2.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QVERIFY(model2.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(topLevelIndex));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::DoNotUseParent));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::ParentIsInvalid));
QTest::ignoreMessage(QtWarningMsg, ignorePattern);
QVERIFY(!model.checkIndex(topLevelIndex, QAbstractItemModel::CheckIndexOption::IndexIsValid));
}
QTEST_MAIN(tst_QAbstractItemModel)
#include "tst_qabstractitemmodel.moc"