tewip-ai 0.48.0 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -927,7 +927,7 @@ That last section is the part no SQL linter does: it searches your own code for
|
|
|
927
927
|
away. "This drops `users.nickname`, and four files still read it" is a different sentence from "dropping a
|
|
928
928
|
column is risky", and it is the one that stops the outage.
|
|
929
929
|
|
|
930
|
-
It reads **SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, EF Core and Liquibase** migrations — which covers Prisma, Drizzle, Flyway
|
|
930
|
+
It reads **SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, Laravel, EF Core and Liquibase** migrations — which covers Prisma, Drizzle, Flyway
|
|
931
931
|
and golang-migrate too, since those all emit plain `.sql`. It judges **only the forward path** (a rollback step
|
|
932
932
|
contains the mirror image of the change, so reading it says a migration that creates a table destroys
|
|
933
933
|
one), and stays quiet when the team already did the safe thing — `atomic = False`, `algorithm:
|
package/dist/cli/help.js
CHANGED
|
@@ -167,7 +167,7 @@ Usage:
|
|
|
167
167
|
that break the version still running during a deploy, destroy data,
|
|
168
168
|
block writes while they run, or cannot be undone - and searches your
|
|
169
169
|
code for what still uses the column it takes away. Reads SQL,
|
|
170
|
-
Django, Rails, Alembic, TypeORM, Sequelize, Knex, EF Core and
|
|
170
|
+
Django, Rails, Alembic, TypeORM, Sequelize, Knex, Laravel, EF Core and
|
|
171
171
|
Liquibase - so Prisma, Drizzle, Flyway and golang-migrate too,
|
|
172
172
|
since those emit plain SQL - and says so when it cannot read one.
|
|
173
173
|
It judges only the forward path, never the rollback. No cost.
|
|
@@ -50,7 +50,23 @@ function withoutNonMarkup(source, jsLike) {
|
|
|
50
50
|
.replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
|
|
51
51
|
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, blank)
|
|
52
52
|
.replace(/<style\b[\s\S]*?<\/style\s*>/gi, blank);
|
|
53
|
-
|
|
53
|
+
// A template expression is code, not markup, and real templates put markup *inside* it: GOV.UK's
|
|
54
|
+
// own examples gallery passes `{{ govukLabel({ "text": "<h1> govuk-label--xl" }) }}`, and reading
|
|
55
|
+
// that `<h1>` as a heading poisoned the outline so badly that twelve heading skips were reported
|
|
56
|
+
// on a page whose headings are perfectly ordered - on the design system built by the UK
|
|
57
|
+
// government's accessibility specialists, which is as wrong as this check can be.
|
|
58
|
+
//
|
|
59
|
+
// The delimiters are kept and only the contents blanked, which matters: `<button>{{ label }}</button>`
|
|
60
|
+
// must still read as content this file cannot see, so the button is asked about rather than
|
|
61
|
+
// called empty, and `alt="{{ alt }}"` must still count as an alt attribute that is present.
|
|
62
|
+
const withoutExpressions = stripped
|
|
63
|
+
// A template comment is a comment: Jinja, Nunjucks and Twig write `{# ... #}`, and Django and
|
|
64
|
+
// Jinja also have a `{% comment %}` block. Neither renders, so neither is markup.
|
|
65
|
+
.replace(/\{#[\s\S]*?#\}/g, blank)
|
|
66
|
+
.replace(/\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/gi, blank)
|
|
67
|
+
.replace(/\{\{[\s\S]*?\}\}/g, (m) => `{{${m.slice(2, -2).replace(/[^\n]/g, ' ')}}}`)
|
|
68
|
+
.replace(/\{%[\s\S]*?%\}/g, (m) => `{%${m.slice(2, -2).replace(/[^\n]/g, ' ')}%}`);
|
|
69
|
+
return jsLike ? withoutExpressions.replace(/`(?:[^`\\]|\\[\s\S])*`/g, blank) : withoutExpressions;
|
|
54
70
|
}
|
|
55
71
|
/** Files whose markup is embedded in JavaScript, where a backtick string is not page markup. */
|
|
56
72
|
export const JS_LIKE = /\.(?:[cm]?[jt]sx?|vue|svelte|astro)$/i;
|
|
@@ -144,4 +144,51 @@ const ALEMBIC = [
|
|
|
144
144
|
// empty on purpose and every finding comes from reading that SQL, which is the same reader the
|
|
145
145
|
// `.sql` dialect uses. The exhaustive Record is what made this a compile error rather than a
|
|
146
146
|
// TypeORM migration silently producing no findings at all.
|
|
147
|
+
/**
|
|
148
|
+
* Laravel. Found missing by running Tewip over firefly-iii, a real finance application with **61
|
|
149
|
+
* migrations Tewip could not read a word of** - and Laravel is one of the most used web frameworks
|
|
150
|
+
* there is, while `schema` advertised nine dialects.
|
|
151
|
+
*
|
|
152
|
+
* Structurally it is Knex's shape: the table is named by `Schema::table('accounts', ...)` and the
|
|
153
|
+
* operation happens inside the closure, so the two are matched together across the lines between.
|
|
154
|
+
*/
|
|
155
|
+
export const LARAVEL = [
|
|
156
|
+
{
|
|
157
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->dropColumn\(\s*${STR}`, 'i'),
|
|
158
|
+
risk: 'breaks-old-code',
|
|
159
|
+
what: (m) => `drops ${m[1]}.${m[2]}`,
|
|
160
|
+
instead: 'deploy code that no longer selects or writes the column, then drop it in a later migration. Eloquent caches the column list, so the running version keeps selecting it',
|
|
161
|
+
table: (m) => m[1],
|
|
162
|
+
column: (m) => m[2],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->renameColumn\(\s*${STR}`, 'i'),
|
|
166
|
+
risk: 'breaks-old-code',
|
|
167
|
+
what: (m) => `renames ${m[1]}.${m[2]}`,
|
|
168
|
+
instead: 'add the new column, write to both, move the readers, then remove the old one',
|
|
169
|
+
table: (m) => m[1],
|
|
170
|
+
column: (m) => m[2],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
find: new RegExp(String.raw `Schema::drop(?:IfExists)?\(\s*${STR}`, 'i'),
|
|
174
|
+
risk: 'destroys-data',
|
|
175
|
+
what: (m) => `drops the table ${m[1]}`,
|
|
176
|
+
instead: 'stop reading the table, deploy that, then drop it once nothing refers to it',
|
|
177
|
+
table: (m) => m[1],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->index\(`, 'i'),
|
|
181
|
+
risk: 'locks-writes',
|
|
182
|
+
what: (m) => `builds an index on ${m[1]} while holding a lock that blocks writes to it`,
|
|
183
|
+
instead: 'build it with a raw CREATE INDEX CONCURRENTLY through DB::statement, or accept the lock if the table is small',
|
|
184
|
+
table: (m) => m[1],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
find: new RegExp(String.raw `Schema::(?:table|connection\(.*?\)->table)\(\s*${STR}[\s\S]{0,600}?->foreign\(`, 'i'),
|
|
188
|
+
risk: 'locks-writes',
|
|
189
|
+
what: (m) => `adds a foreign key to ${m[1]}, which checks every existing row under a lock on both tables`,
|
|
190
|
+
instead: 'add it NOT VALID and validate it afterwards, which takes a weaker lock',
|
|
191
|
+
table: (m) => m[1],
|
|
192
|
+
},
|
|
193
|
+
];
|
|
147
194
|
export { DJANGO, RAILS, ALEMBIC };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALEMBIC, DJANGO, RAILS } from './dialect-rules.js';
|
|
1
|
+
import { ALEMBIC, DJANGO, LARAVEL, RAILS } from './dialect-rules.js';
|
|
2
2
|
import { EFCORE, KNEX, LIQUIBASE, SEQUELIZE } from './dialect-rules-node.js';
|
|
3
3
|
export function dialectOf(file, text) {
|
|
4
4
|
const path = file.split('\\').join('/').toLowerCase();
|
|
@@ -17,6 +17,10 @@ export function dialectOf(file, text) {
|
|
|
17
17
|
// strings passed to queryRunner.query(). The SQL is right there and perfectly readable; only the
|
|
18
18
|
// wrapper was unfamiliar, so every TypeORM project was told Tewip could not read its migrations
|
|
19
19
|
// at all - and on a Node backend that is most of them.
|
|
20
|
+
// Laravel, before the generic checks: its migrations are PHP and would otherwise fall through
|
|
21
|
+
// to 'unknown'. firefly-iii has 61 of them and Tewip could not read a word.
|
|
22
|
+
if (/Illuminate.Database.Migrations.Migration|\bSchema::(?:table|create|drop|dropIfExists|connection)\s*\(/.test(text))
|
|
23
|
+
return 'laravel';
|
|
20
24
|
if (/\bMigrationInterface\b|\bqueryRunner\.query\s*\(/.test(text))
|
|
21
25
|
return 'typeorm';
|
|
22
26
|
if (/\bqueryInterface\.\w+\s*\(/.test(text))
|
|
@@ -40,6 +44,7 @@ const BY_DIALECT = {
|
|
|
40
44
|
knex: KNEX,
|
|
41
45
|
efcore: EFCORE,
|
|
42
46
|
liquibase: LIQUIBASE,
|
|
47
|
+
laravel: LARAVEL,
|
|
43
48
|
};
|
|
44
49
|
/** Findings from a framework migration, with the line each one sits on. */
|
|
45
50
|
export function readFramework(dialect, text) {
|
|
@@ -111,6 +116,8 @@ export function forwardOnly(dialect, text) {
|
|
|
111
116
|
(dialect === 'sequelize' && /^\s*down\s*:/.test(line)) ||
|
|
112
117
|
(dialect === 'knex' && /^\s*(?:exports\.down|module\.exports\.down)\s*=/.test(line)) ||
|
|
113
118
|
(dialect === 'efcore' && /^\s*protected\s+override\s+void\s+Down\s*\(/.test(line)) ||
|
|
119
|
+
// Laravel's rollback, which in real files often comes *before* up().
|
|
120
|
+
(dialect === 'laravel' && /^\s*(?:public\s+)?function\s+down\s*\(/.test(line)) ||
|
|
114
121
|
// Liquibase puts the reverse inside the change itself rather than in a second method.
|
|
115
122
|
(dialect === 'liquibase' && /^\s*<rollback\b/.test(line));
|
|
116
123
|
if (down) {
|
|
@@ -70,7 +70,7 @@ export function readMigration(root, file) {
|
|
|
70
70
|
file,
|
|
71
71
|
dialect,
|
|
72
72
|
findings: [],
|
|
73
|
-
unread: 'Tewip does not recognise how this migration is written, so it says nothing about it rather than passing it. It reads SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, EF Core and Liquibase migrations',
|
|
73
|
+
unread: 'Tewip does not recognise how this migration is written, so it says nothing about it rather than passing it. It reads SQL, Django, Rails, Alembic, TypeORM, Sequelize, Knex, Laravel, EF Core and Liquibase migrations',
|
|
74
74
|
irreversible: false,
|
|
75
75
|
};
|
|
76
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tewip-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"mcpName": "io.github.hmamut39/tewip",
|
|
5
5
|
"description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
|
|
6
6
|
"keywords": [
|