@deckbuilderkit/editor 0.0.0-stage → 0.1.1
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/package.json +53 -4
- package/src/ArchiveView.tsx +111 -0
- package/src/CodeEditor.tsx +265 -0
- package/src/CodeView.tsx +67 -0
- package/src/DiffView.tsx +139 -0
- package/src/EnvView.tsx +42 -0
- package/src/FilePreview.tsx +108 -0
- package/src/FontView.tsx +48 -0
- package/src/HarView.tsx +70 -0
- package/src/HexView.tsx +28 -0
- package/src/HtmlPreview.tsx +23 -0
- package/src/MarkdownEditor.tsx +90 -0
- package/src/MarkdownToc.tsx +135 -0
- package/src/MarkdownView.tsx +84 -0
- package/src/MediaView.tsx +17 -0
- package/src/NotebookView.tsx +74 -0
- package/src/PdfView.tsx +289 -0
- package/src/RequestEditor.tsx +419 -0
- package/src/ResponseView.tsx +89 -0
- package/src/SqlGraph.tsx +322 -0
- package/src/SqlSteps.tsx +487 -0
- package/src/SqliteView.tsx +68 -0
- package/src/SvgView.tsx +231 -0
- package/src/TableView.tsx +85 -0
- package/src/VarSuggest.tsx +125 -0
- package/src/curl-file.ts +70 -0
- package/src/editorOptions.ts +83 -0
- package/src/editorOptionsContext.ts +4 -0
- package/src/fileKinds.ts +69 -0
- package/src/http-client.ts +9 -0
- package/src/http-import.ts +594 -0
- package/src/http.ts +251 -0
- package/src/import-warning.ts +8 -0
- package/src/index.ts +56 -0
- package/src/languages.ts +107 -0
- package/src/markdown.ts +123 -0
- package/src/markdownLive.ts +289 -0
- package/src/options.ts +3 -0
- package/src/pdf-url.d.ts +4 -0
- package/src/sql/format.ts +637 -0
- package/src/sql/formatOptions.ts +71 -0
- package/src/sql/lineage.ts +365 -0
- package/src/sql/lint.ts +951 -0
- package/src/sql/markerHighlight.ts +50 -0
- package/src/sql/markers.ts +138 -0
- package/src/sql/outline.ts +662 -0
- package/src/sql/statements.ts +83 -0
- package/src/sql/tokens.ts +156 -0
- package/src/sql/xmlQuery.ts +249 -0
- package/src/sqlProblems.ts +40 -0
- package/src/table.ts +89 -0
- package/src/text-save.ts +28 -0
- package/src/theme.ts +86 -0
- package/src/toc.ts +11 -0
- package/src/variables.ts +123 -0
- package/README.md +0 -3
package/src/sql/lint.ts
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isNameTok,
|
|
3
|
+
isPunct,
|
|
4
|
+
matchParens,
|
|
5
|
+
nameKey,
|
|
6
|
+
parseAlias,
|
|
7
|
+
parseName,
|
|
8
|
+
sqlOutline,
|
|
9
|
+
word,
|
|
10
|
+
type Ctx,
|
|
11
|
+
type SqlJoinType,
|
|
12
|
+
type SqlOutline,
|
|
13
|
+
type SqlStatementOutline,
|
|
14
|
+
} from './outline.ts';
|
|
15
|
+
import { sqlMarkers, sqlParams } from './markers.ts';
|
|
16
|
+
import { isPlsqlStart } from './statements.ts';
|
|
17
|
+
import { tokenize, type Token } from './tokens.ts';
|
|
18
|
+
|
|
19
|
+
export type SqlLintCode =
|
|
20
|
+
| 'nullCompare'
|
|
21
|
+
| 'notInNullable'
|
|
22
|
+
| 'leftJoinUndone'
|
|
23
|
+
| 'unusedCte'
|
|
24
|
+
| 'duplicateCte'
|
|
25
|
+
| 'duplicateAlias'
|
|
26
|
+
| 'unbalancedParens'
|
|
27
|
+
| 'unionDistinct'
|
|
28
|
+
| 'selectStar'
|
|
29
|
+
| 'cartesianJoin'
|
|
30
|
+
| 'paramOnlyInCondition'
|
|
31
|
+
| 'markerFragmentNoParam'
|
|
32
|
+
| 'paramGap'
|
|
33
|
+
| 'unguardedIn'
|
|
34
|
+
| 'markerUnterminated'
|
|
35
|
+
| 'markerNoFragment'
|
|
36
|
+
| 'setColumnCount'
|
|
37
|
+
| 'rownumOrder'
|
|
38
|
+
| 'fetchNoOrder'
|
|
39
|
+
| 'constantCondition'
|
|
40
|
+
| 'unusedOuterJoin'
|
|
41
|
+
| 'unknownAlias'
|
|
42
|
+
| 'duplicateColumn'
|
|
43
|
+
| 'distinctGroupBy'
|
|
44
|
+
| 'distinctParens'
|
|
45
|
+
| 'emptyStatement'
|
|
46
|
+
| 'emptyStringCompare'
|
|
47
|
+
| 'rownumNever'
|
|
48
|
+
| 'mixedJoinSyntax';
|
|
49
|
+
|
|
50
|
+
export type SqlLintSeverity = 'error' | 'warning' | 'info';
|
|
51
|
+
|
|
52
|
+
export interface SqlProblem {
|
|
53
|
+
code: SqlLintCode;
|
|
54
|
+
severity: SqlLintSeverity;
|
|
55
|
+
from: number;
|
|
56
|
+
to: number;
|
|
57
|
+
/** Values for the i18n message, e.g. {name} or {alias}. */
|
|
58
|
+
params?: Record<string, string>;
|
|
59
|
+
/** CTE name, or '' for the main query, when the problem sits inside a step. */
|
|
60
|
+
step?: string;
|
|
61
|
+
/** Index of the statement in the outline. */
|
|
62
|
+
statement: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const SQL_LINT_SEVERITY: Readonly<Record<SqlLintCode, SqlLintSeverity>> = {
|
|
66
|
+
nullCompare: 'error',
|
|
67
|
+
notInNullable: 'warning',
|
|
68
|
+
leftJoinUndone: 'warning',
|
|
69
|
+
unusedCte: 'warning',
|
|
70
|
+
duplicateCte: 'error',
|
|
71
|
+
duplicateAlias: 'error',
|
|
72
|
+
unbalancedParens: 'error',
|
|
73
|
+
unionDistinct: 'info',
|
|
74
|
+
selectStar: 'info',
|
|
75
|
+
cartesianJoin: 'warning',
|
|
76
|
+
paramOnlyInCondition: 'warning',
|
|
77
|
+
markerFragmentNoParam: 'warning',
|
|
78
|
+
paramGap: 'info',
|
|
79
|
+
unguardedIn: 'warning',
|
|
80
|
+
markerUnterminated: 'warning',
|
|
81
|
+
markerNoFragment: 'warning',
|
|
82
|
+
setColumnCount: 'error',
|
|
83
|
+
rownumOrder: 'warning',
|
|
84
|
+
fetchNoOrder: 'warning',
|
|
85
|
+
constantCondition: 'warning',
|
|
86
|
+
unusedOuterJoin: 'warning',
|
|
87
|
+
unknownAlias: 'error',
|
|
88
|
+
duplicateColumn: 'warning',
|
|
89
|
+
distinctGroupBy: 'info',
|
|
90
|
+
distinctParens: 'info',
|
|
91
|
+
emptyStatement: 'info',
|
|
92
|
+
emptyStringCompare: 'error',
|
|
93
|
+
rownumNever: 'error',
|
|
94
|
+
mixedJoinSyntax: 'error',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const TRIVIA = new Set(['whitespace', 'newline', 'lineComment', 'blockComment', 'slash']);
|
|
98
|
+
const QUERY_START = new Set(['select', 'with']);
|
|
99
|
+
const COMPARE = new Set(['=', '<>', '!=', '^=', '~=']);
|
|
100
|
+
const NULL_SAFE = new Set(['nvl', 'nvl2', 'coalesce', 'decode', 'isnull', 'ifnull']);
|
|
101
|
+
const JOIN_WORDS = new Set(['inner', 'left', 'right', 'full', 'cross', 'natural', 'outer', 'join', 'apply']);
|
|
102
|
+
const STAR_BEFORE = new Set(['select', 'distinct', 'unique', 'all']);
|
|
103
|
+
const NOT_COLUMN = new Set([
|
|
104
|
+
'sysdate', 'systimestamp', 'current_date', 'current_timestamp', 'localtimestamp', 'rownum', 'rowid', 'level',
|
|
105
|
+
'user', 'true', 'false', 'date', 'timestamp', 'interval', 'like', 'escape', 'exists', 'prior',
|
|
106
|
+
]);
|
|
107
|
+
const NOT_BARE = new Set([
|
|
108
|
+
'else', 'asc', 'desc', 'nulls', 'first', 'last', 'between', 'rows', 'row', 'only', 'next', 'siblings', 'any', 'some',
|
|
109
|
+
'ties', 'percent', 'within', 'keep', 'nocycle', 'day', 'month', 'year', 'hour', 'minute', 'second', 'to',
|
|
110
|
+
]);
|
|
111
|
+
const NOT_ALIAS_AFTER = new Set(['prior', 'connect_by_root', 'date', 'timestamp', 'interval', 'distinct', 'unique', 'all']);
|
|
112
|
+
const SET_OPS = new Set(['union', 'intersect', 'minus', 'except']);
|
|
113
|
+
const CMP_ALL = new Set(['=', '<>', '!=', '^=', '~=', '>=', '<=', '>', '<']);
|
|
114
|
+
const FLIP: Record<string, string> = { '<': '>', '>': '<', '<=': '>=', '>=': '<=', '=': '=' };
|
|
115
|
+
const ANSI_JOIN = new Set<ItemJoin>(['inner', 'left', 'right', 'full', 'cross']);
|
|
116
|
+
const PSEUDO_PREFIX = /^(sys|sys_\w*|dbms_\w*|utl_\w*|apex_\w*|owa_\w*|htp|htf|ctx_\w*|mdsys|xdb|public|new|old|standard)$|pkg|pck|pack/;
|
|
117
|
+
|
|
118
|
+
type ItemJoin = SqlJoinType | 'first';
|
|
119
|
+
interface Item {
|
|
120
|
+
join: ItemJoin;
|
|
121
|
+
key: string;
|
|
122
|
+
table: string;
|
|
123
|
+
label: string;
|
|
124
|
+
from: number;
|
|
125
|
+
to: number;
|
|
126
|
+
keyFrom: number;
|
|
127
|
+
keyTo: number;
|
|
128
|
+
complex: boolean;
|
|
129
|
+
}
|
|
130
|
+
interface Block {
|
|
131
|
+
items: Item[];
|
|
132
|
+
where: Array<[number, number]>;
|
|
133
|
+
ons: Array<[number, number, number]>;
|
|
134
|
+
unsure: boolean;
|
|
135
|
+
start: number;
|
|
136
|
+
end: number;
|
|
137
|
+
selFrom: number;
|
|
138
|
+
selTo: number;
|
|
139
|
+
distinct: number;
|
|
140
|
+
group: boolean;
|
|
141
|
+
order: boolean;
|
|
142
|
+
fetch: number[];
|
|
143
|
+
parent: Block | null;
|
|
144
|
+
named: boolean;
|
|
145
|
+
first: boolean;
|
|
146
|
+
}
|
|
147
|
+
interface Frame {
|
|
148
|
+
own: boolean;
|
|
149
|
+
block: Block | null;
|
|
150
|
+
parent: Block | null;
|
|
151
|
+
named: boolean;
|
|
152
|
+
setOp: number;
|
|
153
|
+
clause: string;
|
|
154
|
+
expect: boolean;
|
|
155
|
+
pending: ItemJoin | null;
|
|
156
|
+
joinWords: string[];
|
|
157
|
+
sec: 'where' | 'on' | null;
|
|
158
|
+
secStart: number;
|
|
159
|
+
set: boolean;
|
|
160
|
+
exists: boolean;
|
|
161
|
+
derived: boolean;
|
|
162
|
+
}
|
|
163
|
+
interface Star {
|
|
164
|
+
from: number;
|
|
165
|
+
to: number;
|
|
166
|
+
exists: boolean;
|
|
167
|
+
}
|
|
168
|
+
type Emit = (code: SqlLintCode, from: number, to: number, params?: Record<string, string>) => void;
|
|
169
|
+
|
|
170
|
+
interface Ref {
|
|
171
|
+
key: string;
|
|
172
|
+
text: string;
|
|
173
|
+
wrapped: boolean;
|
|
174
|
+
branch: number;
|
|
175
|
+
}
|
|
176
|
+
interface Conj {
|
|
177
|
+
from: number;
|
|
178
|
+
to: number;
|
|
179
|
+
refs: Ref[];
|
|
180
|
+
bare: number;
|
|
181
|
+
branches: number;
|
|
182
|
+
isNull: boolean;
|
|
183
|
+
hasCase: boolean;
|
|
184
|
+
plus: boolean;
|
|
185
|
+
hasSub: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const isNameLike = (t: Token | undefined): boolean =>
|
|
189
|
+
t !== undefined && (t.kind === 'identifier' || t.kind === 'keyword' || t.kind === 'quotedIdentifier');
|
|
190
|
+
|
|
191
|
+
function closeAt(ctx: Ctx, i: number): number {
|
|
192
|
+
const m = ctx.match[i] ?? -1;
|
|
193
|
+
return m > i ? m : ctx.ts.length - 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function operandStart(ctx: Ctx, k: number): number {
|
|
197
|
+
const { ts, match } = ctx;
|
|
198
|
+
if (k < 0) return ts[0]!.from;
|
|
199
|
+
if (isPunct(ctx, k, ')') && (match[k] ?? -1) >= 0) {
|
|
200
|
+
k = match[k]!;
|
|
201
|
+
if (isNameLike(ts[k - 1])) k--;
|
|
202
|
+
}
|
|
203
|
+
while (k >= 2 && isPunct(ctx, k - 1, '.') && isNameLike(ts[k - 2])) k -= 2;
|
|
204
|
+
return ts[k]!.from;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function operandEnd(ctx: Ctx, k: number): number {
|
|
208
|
+
const { ts } = ctx;
|
|
209
|
+
if (k >= ts.length) return ts[ts.length - 1]!.to;
|
|
210
|
+
if (isPunct(ctx, k, '(')) return ts[closeAt(ctx, k)]!.to;
|
|
211
|
+
while (isPunct(ctx, k + 1, '.') && isNameLike(ts[k + 2])) k += 2;
|
|
212
|
+
if (isPunct(ctx, k + 1, '(')) k = closeAt(ctx, k + 1);
|
|
213
|
+
return ts[k]!.to;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface Scan {
|
|
217
|
+
blocks: Block[];
|
|
218
|
+
stars: Star[];
|
|
219
|
+
owner: Array<Block | null>;
|
|
220
|
+
setOps: Array<[Block, number, Block]>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function scan(ctx: Ctx, emit: Emit): Scan {
|
|
224
|
+
const { ts, match } = ctx;
|
|
225
|
+
const n = ts.length;
|
|
226
|
+
const blocks: Block[] = [];
|
|
227
|
+
const stars: Star[] = [];
|
|
228
|
+
const owner: Array<Block | null> = new Array<Block | null>(n).fill(null);
|
|
229
|
+
const setOps: Array<[Block, number, Block]> = [];
|
|
230
|
+
const frame = (own: boolean, block: Block | null, exists = false, derived = false, parent: Block | null = null, named = false): Frame => ({
|
|
231
|
+
own, block, parent, named, setOp: -1, clause: 'other', expect: false, pending: null, joinWords: [], sec: null, secStart: 0,
|
|
232
|
+
set: false, exists, derived,
|
|
233
|
+
});
|
|
234
|
+
const stack: Frame[] = [frame(true, null)];
|
|
235
|
+
|
|
236
|
+
const closeSec = (f: Frame, at: number): void => {
|
|
237
|
+
if (f.sec && f.block && at > f.secStart) {
|
|
238
|
+
if (f.sec === 'where') f.block.where.push([f.secStart, at]);
|
|
239
|
+
else f.block.ons.push([f.block.items.length - 1, f.secStart, at]);
|
|
240
|
+
}
|
|
241
|
+
f.sec = null;
|
|
242
|
+
};
|
|
243
|
+
const closeSel = (f: Frame, at: number): void => {
|
|
244
|
+
if (f.block && f.clause === 'select' && f.block.selTo < 0) f.block.selTo = at;
|
|
245
|
+
};
|
|
246
|
+
const addItem = (
|
|
247
|
+
f: Frame, keyText: string, from: number, to: number, keyFrom: number, keyTo: number, complex: boolean, table = '',
|
|
248
|
+
): void => {
|
|
249
|
+
f.block!.items.push({
|
|
250
|
+
join: f.pending ?? 'first', key: keyText ? nameKey(keyText) : '', table: table ? nameKey(table) : '', label: keyText,
|
|
251
|
+
from, to, keyFrom, keyTo, complex,
|
|
252
|
+
});
|
|
253
|
+
f.pending = null;
|
|
254
|
+
f.expect = false;
|
|
255
|
+
f.joinWords = [];
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
for (let i = 0; i < n; i++) {
|
|
259
|
+
const t = ts[i]!;
|
|
260
|
+
const f = stack[stack.length - 1]!;
|
|
261
|
+
owner[i] = f.block;
|
|
262
|
+
if (t.kind === 'punctuation' && t.text === '(') {
|
|
263
|
+
if (QUERY_START.has(word(ctx, i + 1))) {
|
|
264
|
+
const derived = f.own && f.expect && f.block !== null;
|
|
265
|
+
const named = derived || (word(ctx, i - 1) === 'as' && !isPunct(ctx, i - 2, ')'));
|
|
266
|
+
stack.push(frame(true, null, word(ctx, i - 1) === 'exists', derived, f.block, named));
|
|
267
|
+
} else {
|
|
268
|
+
if (f.own && f.expect && f.block) {
|
|
269
|
+
f.block.unsure = true;
|
|
270
|
+
f.expect = false;
|
|
271
|
+
}
|
|
272
|
+
stack.push(frame(false, f.block, false, false, f.parent));
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (t.kind === 'punctuation' && t.text === ')') {
|
|
277
|
+
if (stack.length === 1) continue;
|
|
278
|
+
closeSec(f, i);
|
|
279
|
+
closeSel(f, i);
|
|
280
|
+
stack.pop();
|
|
281
|
+
const p = stack[stack.length - 1]!;
|
|
282
|
+
if (f.derived && p.block) {
|
|
283
|
+
const al = parseAlias(ctx, i + 1, n);
|
|
284
|
+
const open = ts[match[i]! >= 0 ? match[i]! : i]!;
|
|
285
|
+
const aliasTok = al.alias ? ts[al.next - 1]! : t;
|
|
286
|
+
addItem(p, al.alias ?? '', open.from, aliasTok.to, aliasTok.from, aliasTok.to, true);
|
|
287
|
+
i = al.next - 1;
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (t.kind === 'operator') {
|
|
292
|
+
if (COMPARE.has(t.text) && !f.set) {
|
|
293
|
+
if (word(ctx, i + 1) === 'null' && i > 0) emit('nullCompare', operandStart(ctx, i - 1), ts[i + 1]!.to);
|
|
294
|
+
else if (word(ctx, i - 1) === 'null') emit('nullCompare', ts[i - 1]!.from, operandEnd(ctx, i + 1));
|
|
295
|
+
else if (ts[i + 1]?.kind === 'string' && ts[i + 1]!.text === "''" && i > 0) {
|
|
296
|
+
emit('emptyStringCompare', operandStart(ctx, i - 1), ts[i + 1]!.to);
|
|
297
|
+
} else if (ts[i - 1]?.kind === 'string' && ts[i - 1]!.text === "''") {
|
|
298
|
+
emit('emptyStringCompare', ts[i - 1]!.from, operandEnd(ctx, i + 1));
|
|
299
|
+
}
|
|
300
|
+
} else if (t.text === '*' && f.own && f.block && f.clause === 'select') {
|
|
301
|
+
const pv = ts[i - 1];
|
|
302
|
+
if (pv && ((pv.kind === 'punctuation' && (pv.text === ',' || pv.text === '.')) || STAR_BEFORE.has(word(ctx, i - 1)))) {
|
|
303
|
+
stars.push({ from: t.from, to: t.to, exists: f.exists });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const lw = word(ctx, i);
|
|
309
|
+
if (lw === 'not' && word(ctx, i + 1) === 'in' && isPunct(ctx, i + 2, '(') && QUERY_START.has(word(ctx, i + 3))) {
|
|
310
|
+
emit('notInNullable', t.from, ts[i + 1]!.to);
|
|
311
|
+
}
|
|
312
|
+
if (lw === 'union' && word(ctx, i + 1) !== 'all') emit('unionDistinct', t.from, t.to);
|
|
313
|
+
if (!f.own) continue;
|
|
314
|
+
if (f.expect && f.block) {
|
|
315
|
+
if (lw === 'lateral' || lw === 'only') continue;
|
|
316
|
+
const nm = parseName(ctx, i, n);
|
|
317
|
+
if (nm) {
|
|
318
|
+
if (isPunct(ctx, nm.next, '(')) {
|
|
319
|
+
const c = closeAt(ctx, nm.next);
|
|
320
|
+
const al = parseAlias(ctx, c + 1, n);
|
|
321
|
+
const end = al.alias ? ts[al.next - 1]! : ts[c]!;
|
|
322
|
+
addItem(f, al.alias ?? '', nm.from, end.to, end.from, end.to, true);
|
|
323
|
+
i = al.next - 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const al = parseAlias(ctx, nm.next, n);
|
|
327
|
+
const last = nm.name.replace(/@.*$/, '').split('.').pop() ?? nm.name;
|
|
328
|
+
if (al.alias) {
|
|
329
|
+
const at = ts[al.next - 1]!;
|
|
330
|
+
addItem(f, al.alias, nm.from, at.to, at.from, at.to, false, last);
|
|
331
|
+
} else {
|
|
332
|
+
addItem(f, last, nm.from, nm.to, nm.from, nm.to, false, last);
|
|
333
|
+
}
|
|
334
|
+
i = al.next - 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
f.expect = false;
|
|
338
|
+
}
|
|
339
|
+
if (t.kind === 'punctuation' && t.text === ',') {
|
|
340
|
+
if (f.block && f.clause === 'from') {
|
|
341
|
+
closeSec(f, i);
|
|
342
|
+
f.pending = 'comma';
|
|
343
|
+
f.expect = true;
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!lw) continue;
|
|
348
|
+
if (lw === 'set') {
|
|
349
|
+
f.set = true;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (lw === 'where' || lw === 'when' || lw === 'returning') f.set = false;
|
|
353
|
+
if (lw === 'select') {
|
|
354
|
+
closeSec(f, i);
|
|
355
|
+
closeSel(f, i);
|
|
356
|
+
const w1 = word(ctx, i + 1);
|
|
357
|
+
const dist = w1 === 'distinct' || w1 === 'unique';
|
|
358
|
+
const blk: Block = {
|
|
359
|
+
items: [], where: [], ons: [], unsure: false, start: i, end: i, selFrom: dist || w1 === 'all' ? i + 2 : i + 1, selTo: -1,
|
|
360
|
+
distinct: dist ? i + 1 : -1, group: false, order: false, fetch: [], parent: f.parent, named: f.named, first: !f.block,
|
|
361
|
+
};
|
|
362
|
+
if (f.block && f.setOp >= 0) setOps.push([f.block, f.setOp, blk]);
|
|
363
|
+
f.setOp = -1;
|
|
364
|
+
blocks.push(blk);
|
|
365
|
+
f.block = blk;
|
|
366
|
+
f.clause = 'select';
|
|
367
|
+
f.expect = false;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (!f.block) continue;
|
|
371
|
+
if (lw === 'from') {
|
|
372
|
+
if (f.clause === 'select') {
|
|
373
|
+
closeSec(f, i);
|
|
374
|
+
closeSel(f, i);
|
|
375
|
+
f.clause = 'from';
|
|
376
|
+
f.expect = true;
|
|
377
|
+
f.pending = null;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (lw === 'where') {
|
|
382
|
+
closeSec(f, i);
|
|
383
|
+
f.sec = 'where';
|
|
384
|
+
f.secStart = i + 1;
|
|
385
|
+
f.clause = 'where';
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (f.clause === 'from' && JOIN_WORDS.has(lw)) {
|
|
389
|
+
closeSec(f, i);
|
|
390
|
+
if (lw === 'join' || lw === 'apply') {
|
|
391
|
+
const w = f.joinWords;
|
|
392
|
+
f.pending =
|
|
393
|
+
lw === 'apply' ? 'apply'
|
|
394
|
+
: w.includes('left') ? 'left'
|
|
395
|
+
: w.includes('right') ? 'right'
|
|
396
|
+
: w.includes('full') ? 'full'
|
|
397
|
+
: w.includes('cross') ? 'cross'
|
|
398
|
+
: 'inner';
|
|
399
|
+
f.joinWords = [];
|
|
400
|
+
f.expect = true;
|
|
401
|
+
} else f.joinWords.push(lw);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (f.clause === 'from' && lw === 'on') {
|
|
405
|
+
closeSec(f, i);
|
|
406
|
+
f.sec = 'on';
|
|
407
|
+
f.secStart = i + 1;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const nx = word(ctx, i + 1);
|
|
411
|
+
if (
|
|
412
|
+
((lw === 'group' || lw === 'order') && (nx === 'by' || nx === 'siblings')) ||
|
|
413
|
+
(lw === 'connect' && nx === 'by') || (lw === 'start' && nx === 'with') || (lw === 'for' && nx === 'update') ||
|
|
414
|
+
lw === 'having' || lw === 'union' || lw === 'intersect' || lw === 'minus' || lw === 'except' ||
|
|
415
|
+
lw === 'fetch' || lw === 'offset' || lw === 'limit' || lw === 'model' || lw === 'window' || lw === 'returning' ||
|
|
416
|
+
lw === 'using' || lw === 'pivot' || lw === 'unpivot'
|
|
417
|
+
) {
|
|
418
|
+
closeSec(f, i);
|
|
419
|
+
if (lw === 'using' && f.clause === 'from') continue;
|
|
420
|
+
closeSel(f, i);
|
|
421
|
+
if (lw === 'group') f.block.group = true;
|
|
422
|
+
else if (lw === 'order') f.block.order = true;
|
|
423
|
+
else if (SET_OPS.has(lw)) f.setOp = i;
|
|
424
|
+
else if ((lw === 'fetch' && (nx === 'first' || nx === 'next')) || (lw === 'offset' && /^rows?$/.test(word(ctx, i + 2)))) {
|
|
425
|
+
f.block.fetch.push(i);
|
|
426
|
+
}
|
|
427
|
+
f.clause = 'other';
|
|
428
|
+
f.expect = false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const f of stack) {
|
|
432
|
+
closeSec(f, n);
|
|
433
|
+
closeSel(f, n);
|
|
434
|
+
}
|
|
435
|
+
owner.forEach((b, k) => {
|
|
436
|
+
if (b) b.end = k;
|
|
437
|
+
});
|
|
438
|
+
return { blocks, stars, owner, setOps };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function splitTop(ctx: Ctx, a: number, b: number): Array<[number, number]> {
|
|
442
|
+
const out: Array<[number, number]> = [];
|
|
443
|
+
let start = a;
|
|
444
|
+
let d = 0;
|
|
445
|
+
for (let k = a; k < b; k++) {
|
|
446
|
+
if (isPunct(ctx, k, '(')) d++;
|
|
447
|
+
else if (isPunct(ctx, k, ')')) d--;
|
|
448
|
+
else if (d === 0 && isPunct(ctx, k, ',')) {
|
|
449
|
+
out.push([start, k]);
|
|
450
|
+
start = k + 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (b > start) out.push([start, b]);
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const selectItems = (ctx: Ctx, blk: Block): Array<[number, number]> =>
|
|
458
|
+
blk.selTo > blk.selFrom ? splitTop(ctx, blk.selFrom, blk.selTo) : [];
|
|
459
|
+
|
|
460
|
+
const isStarItem = (ctx: Ctx, b: number): boolean => ctx.ts[b - 1]?.kind === 'operator' && ctx.ts[b - 1]!.text === '*';
|
|
461
|
+
|
|
462
|
+
function chainEnd(ctx: Ctx, k: number): number {
|
|
463
|
+
if (!isNameLike(ctx.ts[k])) return -1;
|
|
464
|
+
while (isPunct(ctx, k + 1, '.') && isNameLike(ctx.ts[k + 2])) k += 2;
|
|
465
|
+
return k;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Output name token of a select item, and whether it is an alias. */
|
|
469
|
+
function outName(ctx: Ctx, a: number, b: number): { k: number; alias: boolean } | null {
|
|
470
|
+
const { ts } = ctx;
|
|
471
|
+
if (b - a >= 3 && word(ctx, b - 2) === 'as' && isNameLike(ts[b - 1])) return { k: b - 1, alias: true };
|
|
472
|
+
if (b - a >= 2 && isNameTok(ctx, b - 1, b) && !NOT_BARE.has(word(ctx, b - 1))) {
|
|
473
|
+
const p = ts[b - 2]!;
|
|
474
|
+
const ok =
|
|
475
|
+
(p.kind === 'punctuation' && p.text === ')') || p.kind === 'number' || p.kind === 'string' ||
|
|
476
|
+
(isNameLike(p) && !NOT_ALIAS_AFTER.has(word(ctx, b - 2)) && (p.kind === 'quotedIdentifier' || isNameTok(ctx, b - 2, b)));
|
|
477
|
+
if (ok) return { k: b - 1, alias: true };
|
|
478
|
+
}
|
|
479
|
+
return chainEnd(ctx, a) === b - 1 ? { k: b - 1, alias: false } : null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function strip(ctx: Ctx, a: number, b: number): [number, number] {
|
|
483
|
+
while (b - a > 1 && isPunct(ctx, a, '(') && ctx.match[a] === b - 1) {
|
|
484
|
+
a++;
|
|
485
|
+
b--;
|
|
486
|
+
}
|
|
487
|
+
return [a, b];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function findPlus(ctx: Ctx, a: number, b: number): number {
|
|
491
|
+
for (let k = a; k < b; k++) {
|
|
492
|
+
if (!isPunct(ctx, k, '(')) continue;
|
|
493
|
+
if (QUERY_START.has(word(ctx, k + 1))) k = closeAt(ctx, k);
|
|
494
|
+
else if (ctx.ts[k + 1]?.text === '+' && isPunct(ctx, k + 2, ')')) return k;
|
|
495
|
+
}
|
|
496
|
+
return -1;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
interface Shared {
|
|
500
|
+
inSpan: (pos: number) => boolean;
|
|
501
|
+
aliasIdx: Set<number>;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function checkMore(ctx: Ctx, blk: Block, sh: Shared, emit: Emit): void {
|
|
505
|
+
const { ts } = ctx;
|
|
506
|
+
const n = ts.length;
|
|
507
|
+
const { items } = blk;
|
|
508
|
+
const sel = selectItems(ctx, blk);
|
|
509
|
+
if (blk.distinct >= 0) {
|
|
510
|
+
if (blk.group) emit('distinctGroupBy', ts[blk.distinct]!.from, ts[blk.distinct]!.to);
|
|
511
|
+
if (isPunct(ctx, blk.distinct + 1, '(') && sel.length > 1) {
|
|
512
|
+
emit('distinctParens', ts[blk.distinct]!.from, ts[closeAt(ctx, blk.distinct + 1)]!.to);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (!blk.order) for (const k of blk.fetch) emit('fetchNoOrder', ts[k]!.from, ts[k]!.to);
|
|
516
|
+
if (blk.named && blk.first) {
|
|
517
|
+
const seen = new Set<string>();
|
|
518
|
+
for (const [a, b] of sel) {
|
|
519
|
+
const o = outName(ctx, a, b);
|
|
520
|
+
if (!o) continue;
|
|
521
|
+
const key = nameKey(ts[o.k]!.text);
|
|
522
|
+
if (seen.has(key)) emit('duplicateColumn', ts[o.k]!.from, ts[o.k]!.to, { name: ts[o.k]!.text });
|
|
523
|
+
seen.add(key);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const where = blk.where.flatMap(([a, b]) => conjuncts(ctx, a, b));
|
|
528
|
+
for (const [a0, b0] of where) {
|
|
529
|
+
const [a, b] = strip(ctx, a0, b0);
|
|
530
|
+
const span = (): [number, number] => [ts[a]!.from, ts[b - 1]!.to];
|
|
531
|
+
if (b - a < 3) continue;
|
|
532
|
+
const head = word(ctx, a) === 'rownum';
|
|
533
|
+
const tail = word(ctx, b - 1) === 'rownum';
|
|
534
|
+
if (!head && !tail) continue;
|
|
535
|
+
const opTok = ts[head ? a + 1 : b - 2]!;
|
|
536
|
+
if (opTok.kind !== 'operator' || !FLIP[opTok.text]) continue;
|
|
537
|
+
const op = head ? opTok.text : FLIP[opTok.text]!;
|
|
538
|
+
if (blk.order && (op === '<' || op === '<=' || op === '=')) emit('rownumOrder', ...span());
|
|
539
|
+
if (b - a !== 3) continue;
|
|
540
|
+
const num = ts[head ? a + 2 : a]!;
|
|
541
|
+
if (num.kind !== 'number') continue;
|
|
542
|
+
const v = Number(num.text);
|
|
543
|
+
if ((op === '>' && v >= 1) || ((op === '>=' || op === '=') && v > 1)) emit('rownumNever', ...span());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const conds = [...where, ...blk.ons.flatMap(([, a, b]) => conjuncts(ctx, a, b))];
|
|
547
|
+
for (const [a0, b0] of conds) {
|
|
548
|
+
const [a, b] = strip(ctx, a0, b0);
|
|
549
|
+
if (!isNameTok(ctx, a, b) || NOT_COLUMN.has(word(ctx, a))) continue;
|
|
550
|
+
const j = chainEnd(ctx, a);
|
|
551
|
+
const op = ts[j + 1];
|
|
552
|
+
if (!op || op.kind !== 'operator' || !CMP_ALL.has(op.text)) continue;
|
|
553
|
+
if (!isNameTok(ctx, j + 2, b) || chainEnd(ctx, j + 2) !== b - 1 || b - 1 - (j + 2) !== j - a) continue;
|
|
554
|
+
let same = true;
|
|
555
|
+
for (let k = 0; k <= j - a && same; k++) same = nameKey(ts[a + k]!.text) === nameKey(ts[j + 2 + k]!.text);
|
|
556
|
+
if (same) emit('constantCondition', ts[a]!.from, ts[b - 1]!.to);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (items.some((it) => ANSI_JOIN.has(it.join))) {
|
|
560
|
+
const ranges = [...blk.where, ...blk.ons.map(([, a, b]): [number, number] => [a, b])].sort((x, y) => x[0] - y[0]);
|
|
561
|
+
for (const [a, b] of ranges) {
|
|
562
|
+
const k = findPlus(ctx, a, b);
|
|
563
|
+
if (k >= 0) {
|
|
564
|
+
emit('mixedJoinSyntax', ts[k]!.from, ts[k + 2]!.to);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const outer = items.filter((it) => it.key && (it.join === 'left' || it.join === 'right' || it.join === 'full'));
|
|
571
|
+
if (blk.unsure || outer.length === 0 || sel.some(([a, b]) => b - a === 1 && isStarItem(ctx, b))) return;
|
|
572
|
+
for (let k = blk.start + 1; k <= blk.end; k++) {
|
|
573
|
+
const w = word(ctx, k);
|
|
574
|
+
if (
|
|
575
|
+
(ts[k]!.kind === 'quotedIdentifier' || (isNameTok(ctx, k, n) && !NOT_COLUMN.has(w) && !NOT_BARE.has(w))) &&
|
|
576
|
+
!isPunct(ctx, k - 1, '.') && !isPunct(ctx, k + 1, '.') && !isPunct(ctx, k + 1, '(') && word(ctx, k - 1) !== 'as' &&
|
|
577
|
+
!sh.aliasIdx.has(k) && !sh.inSpan(ts[k]!.from)
|
|
578
|
+
) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (const it of outer) {
|
|
583
|
+
const k = items.indexOf(it);
|
|
584
|
+
const own = blk.ons.filter((o) => o[0] === k);
|
|
585
|
+
let used = false;
|
|
586
|
+
for (let x = 0; x < n && !used; x++) {
|
|
587
|
+
const t = ts[x]!;
|
|
588
|
+
used =
|
|
589
|
+
isNameLike(t) && isPunct(ctx, x + 1, '.') && nameKey(t.text) === it.key && !(t.from >= it.from && t.to <= it.to) &&
|
|
590
|
+
!own.some(([, a, b]) => x >= a && x < b);
|
|
591
|
+
}
|
|
592
|
+
if (!used) emit('unusedOuterJoin', it.from, it.to, { alias: it.label });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function checkAliases(ctx: Ctx, st: SqlStatementOutline, sc: Scan, sh: Shared, emit: Emit): void {
|
|
597
|
+
if (st.kind !== 'select' && st.kind !== 'insert' && st.kind !== 'create') return;
|
|
598
|
+
const { ts } = ctx;
|
|
599
|
+
const ctes = new Set(st.steps.filter((s) => s.kind === 'cte').map((s) => nameKey(s.name)));
|
|
600
|
+
ts.forEach((t, k) => {
|
|
601
|
+
if (isNameLike(t) && word(ctx, k + 1) === 'as' && isPunct(ctx, k + 2, '(')) ctes.add(nameKey(t.text));
|
|
602
|
+
});
|
|
603
|
+
const scopes = new Map<Block, Set<string> | null>();
|
|
604
|
+
const scope = (b: Block): Set<string> | null => {
|
|
605
|
+
if (scopes.has(b)) return scopes.get(b)!;
|
|
606
|
+
let s: Set<string> | null = null;
|
|
607
|
+
if (!b.unsure) {
|
|
608
|
+
const up = b.parent ? scope(b.parent) : new Set<string>();
|
|
609
|
+
if (up) {
|
|
610
|
+
s = new Set(up);
|
|
611
|
+
for (const it of b.items) {
|
|
612
|
+
if (it.key) s.add(it.key);
|
|
613
|
+
if (it.table) s.add(it.table);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
scopes.set(b, s);
|
|
618
|
+
return s;
|
|
619
|
+
};
|
|
620
|
+
ts.forEach((t, k) => {
|
|
621
|
+
const b = sc.owner[k];
|
|
622
|
+
if (!b || !isNameLike(t) || !isPunct(ctx, k + 1, '.') || !isNameLike(ts[k + 2])) return;
|
|
623
|
+
if (isPunct(ctx, k - 1, '.') || isPunct(ctx, k - 1, '@') || isPunct(ctx, k + 3, '.') || isPunct(ctx, k + 3, '(') || isPunct(ctx, k + 3, '@')) return;
|
|
624
|
+
const col = word(ctx, k + 2);
|
|
625
|
+
const q = nameKey(t.text);
|
|
626
|
+
if (col === 'nextval' || col === 'currval' || PSEUDO_PREFIX.test(q) || ctes.has(q) || sh.inSpan(t.from)) return;
|
|
627
|
+
const s = scope(b);
|
|
628
|
+
if (s && !s.has(q)) emit('unknownAlias', t.from, t.to, { alias: t.text });
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function checkSetOps(ctx: Ctx, sc: Scan, emit: Emit): void {
|
|
633
|
+
for (const [x, op, y] of sc.setOps) {
|
|
634
|
+
const a = selectItems(ctx, x);
|
|
635
|
+
const b = selectItems(ctx, y);
|
|
636
|
+
if (a.length === 0 || b.length === 0 || [...a, ...b].some(([, q]) => isStarItem(ctx, q))) continue;
|
|
637
|
+
if (a.length !== b.length) {
|
|
638
|
+
emit('setColumnCount', ctx.ts[op]!.from, ctx.ts[op]!.to, { left: String(a.length), right: String(b.length) });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function lintTerminators(all: Token[], ol: SqlOutline, out: SqlProblem[]): void {
|
|
644
|
+
let started = false;
|
|
645
|
+
let plsql = false;
|
|
646
|
+
let term = false;
|
|
647
|
+
const hit = (t: Token): void => {
|
|
648
|
+
if (!started && term) {
|
|
649
|
+
const statement = Math.max(0, ol.statements.filter((s) => s.to <= t.from).length - 1);
|
|
650
|
+
out.push({ code: 'emptyStatement', severity: SQL_LINT_SEVERITY.emptyStatement, from: t.from, to: t.to, statement });
|
|
651
|
+
}
|
|
652
|
+
started = false;
|
|
653
|
+
plsql = false;
|
|
654
|
+
term = true;
|
|
655
|
+
};
|
|
656
|
+
for (let i = 0; i < all.length; i++) {
|
|
657
|
+
const t = all[i]!;
|
|
658
|
+
if (t.kind === 'slash') hit(t);
|
|
659
|
+
else if (TRIVIA.has(t.kind)) continue;
|
|
660
|
+
else if (t.kind === 'punctuation' && t.text === ';' && !plsql) hit(t);
|
|
661
|
+
else if (!started) {
|
|
662
|
+
started = true;
|
|
663
|
+
plsql = isPlsqlStart(all, i);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function conjuncts(ctx: Ctx, a: number, b: number): Array<[number, number]> {
|
|
669
|
+
const out: Array<[number, number]> = [];
|
|
670
|
+
let start = a;
|
|
671
|
+
let d = 0;
|
|
672
|
+
let caseD = 0;
|
|
673
|
+
let between = false;
|
|
674
|
+
for (let k = a; k < b; k++) {
|
|
675
|
+
if (isPunct(ctx, k, '(')) d++;
|
|
676
|
+
else if (isPunct(ctx, k, ')')) d--;
|
|
677
|
+
else if (d === 0) {
|
|
678
|
+
const lw = word(ctx, k);
|
|
679
|
+
if (lw === 'case') caseD++;
|
|
680
|
+
else if (lw === 'end' && caseD > 0) caseD--;
|
|
681
|
+
else if (caseD === 0 && lw === 'between') between = true;
|
|
682
|
+
else if (caseD === 0 && lw === 'and') {
|
|
683
|
+
if (between) between = false;
|
|
684
|
+
else {
|
|
685
|
+
if (k > start) out.push([start, k]);
|
|
686
|
+
start = k + 1;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (b > start) out.push([start, b]);
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function analyzeConj(ctx: Ctx, a0: number, b0: number): Conj {
|
|
696
|
+
const { ts, match } = ctx;
|
|
697
|
+
const c: Conj = {
|
|
698
|
+
from: ts[a0]!.from, to: ts[b0 - 1]!.to, refs: [], bare: 0, branches: 1, isNull: false, hasCase: false, plus: false,
|
|
699
|
+
hasSub: false,
|
|
700
|
+
};
|
|
701
|
+
let a = a0;
|
|
702
|
+
let b = b0;
|
|
703
|
+
while (b - a > 1 && isPunct(ctx, a, '(') && match[a] === b - 1) {
|
|
704
|
+
a++;
|
|
705
|
+
b--;
|
|
706
|
+
}
|
|
707
|
+
const fns: string[] = [];
|
|
708
|
+
for (let k = a; k < b; k++) {
|
|
709
|
+
const t = ts[k]!;
|
|
710
|
+
if (t.kind === 'punctuation' && t.text === '(') {
|
|
711
|
+
if (QUERY_START.has(word(ctx, k + 1))) {
|
|
712
|
+
c.hasSub = true;
|
|
713
|
+
const m = match[k] ?? -1;
|
|
714
|
+
k = m > k && m < b ? m : b;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (t.text === '(' && ts[k + 1]?.text === '+' && isPunct(ctx, k + 2, ')')) {
|
|
718
|
+
c.plus = true;
|
|
719
|
+
k += 2;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
fns.push(word(ctx, k - 1));
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (t.kind === 'punctuation' && t.text === ')') {
|
|
726
|
+
fns.pop();
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const lw = word(ctx, k);
|
|
730
|
+
if (lw === 'case') c.hasCase = true;
|
|
731
|
+
else if (lw === 'is' && word(ctx, k + 1) === 'null') c.isNull = true;
|
|
732
|
+
else if (lw === 'or' && fns.length === 0) {
|
|
733
|
+
c.branches++;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (!isNameLike(t) || isPunct(ctx, k - 1, '.')) continue;
|
|
737
|
+
let j = k;
|
|
738
|
+
while (isPunct(ctx, j + 1, '.') && isNameLike(ts[j + 2])) j += 2;
|
|
739
|
+
const call = isPunct(ctx, j + 1, '(') && !(ts[j + 2]?.text === '+' && isPunct(ctx, j + 3, ')'));
|
|
740
|
+
if (!call) {
|
|
741
|
+
if (j > k) {
|
|
742
|
+
const q = ts[j - 2]!;
|
|
743
|
+
c.refs.push({ key: nameKey(q.text), text: q.text, wrapped: fns.some((f) => NULL_SAFE.has(f)), branch: c.branches - 1 });
|
|
744
|
+
} else if (isNameTok(ctx, k, b) && !NOT_COLUMN.has(lw)) c.bare++;
|
|
745
|
+
}
|
|
746
|
+
k = j;
|
|
747
|
+
}
|
|
748
|
+
return c;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function checkBlock(ctx: Ctx, blk: Block, emit: Emit): void {
|
|
752
|
+
const { items } = blk;
|
|
753
|
+
const seen = new Set<string>();
|
|
754
|
+
for (const it of items) {
|
|
755
|
+
if (!it.key) continue;
|
|
756
|
+
if (seen.has(it.key)) emit('duplicateAlias', it.keyFrom, it.keyTo, { alias: it.label });
|
|
757
|
+
seen.add(it.key);
|
|
758
|
+
}
|
|
759
|
+
if (items.length < 2) return;
|
|
760
|
+
const where = blk.where.flatMap(([a, b]) => conjuncts(ctx, a, b)).map(([a, b]) => analyzeConj(ctx, a, b));
|
|
761
|
+
|
|
762
|
+
const nullable = new Map<string, string>();
|
|
763
|
+
items.forEach((it, k) => {
|
|
764
|
+
if (it.key && (it.join === 'left' || it.join === 'full')) nullable.set(it.key, it.label);
|
|
765
|
+
if (it.join === 'right' || it.join === 'full') {
|
|
766
|
+
for (let j = 0; j < k; j++) if (items[j]!.key) nullable.set(items[j]!.key, items[j]!.label);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
if (nullable.size > 0 && !blk.unsure) {
|
|
770
|
+
for (const c of where) {
|
|
771
|
+
if (c.isNull || c.hasCase || c.plus) continue;
|
|
772
|
+
let alias = '';
|
|
773
|
+
let all = true;
|
|
774
|
+
for (let br = 0; br < c.branches && all; br++) {
|
|
775
|
+
const r = c.refs.find((x) => x.branch === br && !x.wrapped && nullable.has(x.key));
|
|
776
|
+
if (!r) all = false;
|
|
777
|
+
else if (!alias) alias = nullable.get(r.key)!;
|
|
778
|
+
}
|
|
779
|
+
if (all && alias) emit('leftJoinUndone', c.from, c.to, { alias });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (blk.unsure || !items.some((it) => it.join === 'comma')) return;
|
|
784
|
+
const parent = items.map((_, k) => k);
|
|
785
|
+
const find = (x: number): number => {
|
|
786
|
+
while (parent[x] !== x) x = parent[x] = parent[parent[x]!]!;
|
|
787
|
+
return x;
|
|
788
|
+
};
|
|
789
|
+
const union = (x: number, y: number): void => {
|
|
790
|
+
parent[find(x)] = find(y);
|
|
791
|
+
};
|
|
792
|
+
const byKey = new Map<string, number[]>();
|
|
793
|
+
items.forEach((it, k) => {
|
|
794
|
+
if (it.key) byKey.set(it.key, [...(byKey.get(it.key) ?? []), k]);
|
|
795
|
+
if (k > 0 && (it.complex || (it.join !== 'comma' && it.join !== 'cross'))) union(k, k - 1);
|
|
796
|
+
});
|
|
797
|
+
const conds: Array<{ c: Conj; item: number }> = where.map((c) => ({ c, item: -1 }));
|
|
798
|
+
for (const [item, a, b] of blk.ons) for (const [x, y] of conjuncts(ctx, a, b)) conds.push({ c: analyzeConj(ctx, x, y), item });
|
|
799
|
+
for (const { c, item } of conds) {
|
|
800
|
+
const idx = new Set<number>();
|
|
801
|
+
for (const r of c.refs) for (const k of byKey.get(r.key) ?? []) idx.add(k);
|
|
802
|
+
if (item >= 0) idx.add(item);
|
|
803
|
+
if (c.hasSub || (c.bare > 0 && idx.size + c.bare >= 2)) return;
|
|
804
|
+
const list = [...idx];
|
|
805
|
+
for (let k = 1; k < list.length; k++) union(list[k]!, list[0]!);
|
|
806
|
+
}
|
|
807
|
+
const min = new Map<number, number>();
|
|
808
|
+
items.forEach((_, k) => {
|
|
809
|
+
const r = find(k);
|
|
810
|
+
if (!min.has(r)) min.set(r, k);
|
|
811
|
+
});
|
|
812
|
+
items.forEach((it, k) => {
|
|
813
|
+
if (k > 0 && it.join === 'comma' && !it.complex && it.key && min.get(find(k)) === k) {
|
|
814
|
+
emit('cartesianJoin', it.from, it.to, { alias: it.label });
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function stepAt(st: SqlStatementOutline, pos: number): string | undefined {
|
|
820
|
+
for (const s of st.steps) if (s.kind === 'cte' && pos >= s.from && pos <= s.to) return s.name;
|
|
821
|
+
for (const s of st.steps) if (s.kind === 'main' && pos >= s.from && pos <= s.to) return '';
|
|
822
|
+
return undefined;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function lintStatement(ts: Token[], st: SqlStatementOutline, index: number, out: SqlProblem[]): void {
|
|
826
|
+
const emit: Emit = (code, from, to, params) => {
|
|
827
|
+
const p: SqlProblem = { code, severity: SQL_LINT_SEVERITY[code], from, to, statement: index };
|
|
828
|
+
if (params) p.params = params;
|
|
829
|
+
const step = stepAt(st, from);
|
|
830
|
+
if (step !== undefined) p.step = step;
|
|
831
|
+
out.push(p);
|
|
832
|
+
};
|
|
833
|
+
const open: Token[] = [];
|
|
834
|
+
for (const t of ts) {
|
|
835
|
+
if (t.kind !== 'punctuation') continue;
|
|
836
|
+
if (t.text === '(') open.push(t);
|
|
837
|
+
else if (t.text === ')' && !open.pop()) emit('unbalancedParens', t.from, t.to);
|
|
838
|
+
}
|
|
839
|
+
for (const t of open) emit('unbalancedParens', t.from, t.to);
|
|
840
|
+
if (st.kind === 'plsql' || ts.length === 0) return;
|
|
841
|
+
|
|
842
|
+
const ctx: Ctx = { ts, match: matchParens(ts), ctes: new Set() };
|
|
843
|
+
const sc = scan(ctx, emit);
|
|
844
|
+
const { blocks, stars } = sc;
|
|
845
|
+
for (const blk of blocks) checkBlock(ctx, blk, emit);
|
|
846
|
+
const spans = blocks.flatMap((b) => b.items.map((it) => [it.from, it.to] as const));
|
|
847
|
+
const aliasIdx = new Set<number>();
|
|
848
|
+
for (const blk of blocks) {
|
|
849
|
+
for (const [a, b] of selectItems(ctx, blk)) {
|
|
850
|
+
const o = outName(ctx, a, b);
|
|
851
|
+
if (o?.alias) aliasIdx.add(o.k);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const sh: Shared = { inSpan: (pos) => spans.some(([a, b]) => pos >= a && pos < b), aliasIdx };
|
|
855
|
+
for (const blk of blocks) checkMore(ctx, blk, sh, emit);
|
|
856
|
+
checkSetOps(ctx, sc, emit);
|
|
857
|
+
checkAliases(ctx, st, sc, sh, emit);
|
|
858
|
+
|
|
859
|
+
const feedsWrite = st.kind === 'insert' || st.kind === 'create';
|
|
860
|
+
for (const s of stars) {
|
|
861
|
+
if (s.exists) continue;
|
|
862
|
+
if (feedsWrite || st.steps.some((x) => x.kind === 'cte' && s.from >= x.from && s.from <= x.to)) emit('selectStar', s.from, s.to);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const names = new Set<string>();
|
|
866
|
+
for (const s of st.steps) {
|
|
867
|
+
if (s.kind !== 'cte' || s.nameFrom === undefined || s.nameTo === undefined) continue;
|
|
868
|
+
const key = nameKey(s.name);
|
|
869
|
+
if (names.has(key)) emit('duplicateCte', s.nameFrom, s.nameTo, { name: s.name });
|
|
870
|
+
else if (s.feeds.length === 0) emit('unusedCte', s.nameFrom, s.nameTo, { name: s.name });
|
|
871
|
+
names.add(key);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function lintMarkers(text: string, ol: SqlOutline, out: SqlProblem[]): void {
|
|
876
|
+
const markers = sqlMarkers(text);
|
|
877
|
+
const params = sqlParams(text);
|
|
878
|
+
const emit = (code: SqlLintCode, from: number, to: number, p?: Record<string, string>): void => {
|
|
879
|
+
let statement = ol.statements.findIndex((s) => from >= s.from && from <= s.to);
|
|
880
|
+
if (statement < 0) statement = 0;
|
|
881
|
+
const problem: SqlProblem = { code, severity: SQL_LINT_SEVERITY[code], from, to, statement };
|
|
882
|
+
if (p) problem.params = p;
|
|
883
|
+
out.push(problem);
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
for (let i = text.indexOf('/*%JS='); i >= 0; i = text.indexOf('/*%JS=', i + 1)) {
|
|
887
|
+
if (!markers.some((m) => m.from === i)) emit('markerUnterminated', i, i + 6);
|
|
888
|
+
}
|
|
889
|
+
for (const m of markers) {
|
|
890
|
+
if (!text.slice(m.fragmentFrom, m.fragmentTo).trim()) emit('markerNoFragment', m.from, m.to);
|
|
891
|
+
else if (m.params.length > 0 && m.fragmentParams.length === 0) emit('markerFragmentNoParam', m.from, m.to);
|
|
892
|
+
}
|
|
893
|
+
for (const p of params) {
|
|
894
|
+
if (p.uses.some((u) => u.kind === 'condition') && p.markers === 0 && p.unguarded === 0) {
|
|
895
|
+
const u = p.uses.find((x) => x.kind === 'condition')!;
|
|
896
|
+
emit('paramOnlyInCondition', u.from, u.to, { n: String(p.n) });
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
for (let k = 1; k < params.length; k++) {
|
|
900
|
+
const prev = params[k - 1]!.n;
|
|
901
|
+
const cur = params[k]!;
|
|
902
|
+
for (let n = prev + 1; n < cur.n; n++) emit('paramGap', cur.firstFrom, cur.firstFrom + String(cur.n).length + 2, { n: String(n) });
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const IN = /\bin\s*\(([^()]*)\)/gi;
|
|
906
|
+
for (const m of text.matchAll(IN)) {
|
|
907
|
+
const inner = m[1]!;
|
|
908
|
+
const base = m.index! + m[0].length - 1 - inner.length;
|
|
909
|
+
for (const u of inner.matchAll(/\{(\d+)\}/g)) {
|
|
910
|
+
const at = base + u.index!;
|
|
911
|
+
if (markers.some((k) => at >= k.from && at < k.fragmentTo)) continue;
|
|
912
|
+
emit('unguardedIn', at, at + u[0].length, { n: u[1]! });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Finds silent logic errors in SQL text (Oracle-first, standard SQL tolerated). Pure; never throws. */
|
|
918
|
+
export function sqlLint(text: string, outline?: SqlOutline): SqlProblem[] {
|
|
919
|
+
const out: SqlProblem[] = [];
|
|
920
|
+
try {
|
|
921
|
+
const ol = outline ?? sqlOutline(text);
|
|
922
|
+
try {
|
|
923
|
+
lintMarkers(text, ol, out);
|
|
924
|
+
} catch {
|
|
925
|
+
// best effort
|
|
926
|
+
}
|
|
927
|
+
const all = tokenize(text);
|
|
928
|
+
try {
|
|
929
|
+
lintTerminators(all, ol, out);
|
|
930
|
+
} catch {
|
|
931
|
+
// best effort
|
|
932
|
+
}
|
|
933
|
+
let k = 0;
|
|
934
|
+
ol.statements.forEach((st, index) => {
|
|
935
|
+
while (k < all.length && all[k]!.from < st.from) k++;
|
|
936
|
+
const ts: Token[] = [];
|
|
937
|
+
while (k < all.length && all[k]!.to <= st.to) {
|
|
938
|
+
if (!TRIVIA.has(all[k]!.kind)) ts.push(all[k]!);
|
|
939
|
+
k++;
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
lintStatement(ts, st, index, out);
|
|
943
|
+
} catch {
|
|
944
|
+
// best effort per statement
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
} catch {
|
|
948
|
+
return out;
|
|
949
|
+
}
|
|
950
|
+
return out.sort((x, y) => x.from - y.from || x.to - y.to);
|
|
951
|
+
}
|