mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
fix(pascal): Delphi units with re-published properties and similar syntax parse whole (#2816)
SynEdit's main unit parsed into one ERROR node on main. A re-published
property, `property OwnsObjects;` at line 303, is one of several Delphi
constructs tree-sitter-pascal has no rule for. Each breaks the parse
locally, and enough of them cost a whole unit: routines after a break
lost their calls or had them credited to the file, their locals showed
up as unit variables, and class members after an unnamed record went
missing.
The Pascal pre-parse now fixes each construct in place, offsets and
newlines kept (signatures still read the source as written):
- a re-published property (`property Cursor default crIBeam;`) is blanked;
- a conditional right after a routine's header that holds no whole body
(`{$IFDEF HASINLINE}inline;{$ENDIF}`) has its directives blanked, and
so does one nested at the start of a conditional that holds no block;
- branches that open the same `begin` keep only the first one, and an
assembler version that opens a body beside a Pascal one is blanked;
- a re-raise is blanked, or written as `;` right before an `end`;
- an unnamed record type (`fIn: record … end`) is written as `string`
with its fields blanked, so the declaration keeps its name;
- a label right before `end` is blanked;
- alternative statements after a case label, or after then/else/do
without a `;`, read as one block: the directives become begin, `;`
and end, so every branch keeps its calls.
The last fix also undoes a regression the header fix caused alone:
in HeidiSQL's SynRegExpr.pas a case statement full of `{$IFDEF}`
alternatives broke the unit once main's accidental resync points were
gone (659 references lost). With all fixes it has 1,712 against
main's 1,681.
Also fixed on the way: a routine nested in another credited the
constants its parent's body reads to itself, when it was the last one
before the body (the value-ref scan pulled in the next sibling, which
is the outer routine's body).
Validation: pascal-delphi-syntax (17 tests, 13 fail on main) and a new
pascal-nested-routines case (fails on main); 14 mutations, one per rule
and three guards, each fail a test. Two pascal-system-calls fixtures
broke the parse on purpose with constructs this now reads; they use an
old-style `begin … end.` initialization and an anonymous record holding
a method instead, which still break it, and still fail when the guard
they cover is removed. Parse census, main -> fix, files
without a unit node: SynEdit 4 -> 1, HeidiSQL 6 -> 1, DelphiMVCFramework
15 -> 1, mORMot2 34 -> 6, DWScript 2100 -> 2086 (its test scripts have
no unit header). Indexed A/B, edges removed/added: SynEdit 1889/2150,
HeidiSQL 2571/3305, DelphiMVCFramework 4915/6227, mORMot2 15298/27195,
DWScript 3758/4570; leveldb and SDWebImage identical. No file loses
more call sites than it gains outside DelphiMVCFramework, where the
six that do lose only wrong edges. Index time within noise (HeidiSQL
+3% wall, +8% CPU for the code now read); the pre-parse itself takes
223 ms over mORMot2's 25 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ab92c90a54
commit
df1cd1e185
@@ -0,0 +1,521 @@
|
||||
/**
|
||||
* Delphi syntax the Pascal grammar has no rule for, which broke whole units
|
||||
* of SynEdit, mORMot2, DWScript and HeidiSQL: a property re-published
|
||||
* without its type (`property OwnsObjects;`, SynEdit.pas from line 303), a
|
||||
* conditional right after a routine's header that holds no whole body, two
|
||||
* branches that open the same `begin`, a re-raise, a field or variable of an
|
||||
* anonymous record type, a label right before `end`, and alternative
|
||||
* statements where the grammar takes one (`OP_X: {$IFDEF U} … {$ELSE} …
|
||||
* {$ENDIF}`). The extractor's preParse blanks each, or writes a word the
|
||||
* grammar knows in its place, offsets kept, so the unit parses whole and the
|
||||
* routines after it keep their calls.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { CodeGraph } from '../src';
|
||||
import { extractFromSource } from '../src/extraction';
|
||||
import { getParser, initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
|
||||
import { preParsePascal } from '../src/extraction/languages/pascal-preparse';
|
||||
|
||||
beforeAll(async () => {
|
||||
await initGrammars();
|
||||
await loadGrammarsForLanguages(['pascal']);
|
||||
});
|
||||
|
||||
/** Whether the unit parses whole: a `unit` node and no error anywhere. */
|
||||
function parsesWhole(code: string): boolean {
|
||||
const tree = getParser('pascal')!.parse(preParsePascal(code))!;
|
||||
try {
|
||||
return !tree.rootNode.hasError && tree.rootNode.namedChildren.some((c) => c.type === 'unit');
|
||||
} finally {
|
||||
tree.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/** Every routine's calls, as `Caller->Callee`, and every node's qualified name, from the unit's extraction. */
|
||||
function extract(code: string, file: string): { calls: string[]; names: string[] } {
|
||||
const result = extractFromSource(file, code);
|
||||
const byId = new Map(result.nodes.map((n) => [n.id, n]));
|
||||
return {
|
||||
calls: result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === 'calls')
|
||||
.map((r) => `${byId.get(r.fromNodeId)?.name}->${r.referenceName}`),
|
||||
names: result.nodes.map((n) => n.qualifiedName),
|
||||
};
|
||||
}
|
||||
|
||||
const OVERRIDES = `unit Marks;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TMarkList = class(TObjectList)
|
||||
protected
|
||||
function GetItem(Index: Integer): TObject;
|
||||
property OwnsObjects;
|
||||
public
|
||||
constructor Create;
|
||||
property Items[Index: Integer]: TObject read GetItem; default;
|
||||
end;
|
||||
|
||||
TEditor = class(TCustomControl)
|
||||
published
|
||||
property Align;
|
||||
property Cursor default crIBeam;
|
||||
class property Tag;
|
||||
property Lines: TStrings read FLines;
|
||||
procedure Paint;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
procedure TEditor.Paint;
|
||||
begin
|
||||
Refresh(1);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const HEADERS = `unit Headers;
|
||||
|
||||
interface
|
||||
|
||||
procedure KeyUp;
|
||||
function Size: Integer;
|
||||
procedure Generate;
|
||||
|
||||
implementation
|
||||
|
||||
procedure KeyUp;
|
||||
{$IFDEF LINUX}
|
||||
var
|
||||
Code: Byte;
|
||||
{$ENDIF}
|
||||
begin
|
||||
Translate(1);
|
||||
end;
|
||||
|
||||
function Size: Integer; {$IFDEF HASINLINE}inline;{$ENDIF}
|
||||
begin
|
||||
Result := Measure(1);
|
||||
end;
|
||||
|
||||
procedure Generate;
|
||||
{$IFDEF LLVM}
|
||||
{$IFDEF LLVM_EXECUTE}
|
||||
var
|
||||
Engine: Pointer;
|
||||
{$ENDIF}
|
||||
{$ENDIF}
|
||||
begin
|
||||
Emit(1);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const BEGINS = `unit Begins;
|
||||
|
||||
interface
|
||||
|
||||
procedure Click;
|
||||
procedure After;
|
||||
|
||||
implementation
|
||||
|
||||
procedure Click;
|
||||
var
|
||||
I: Integer;
|
||||
{$IFDEF FOLDING}
|
||||
Index: Integer;
|
||||
begin
|
||||
Fold(1);
|
||||
if I > 0 then
|
||||
begin
|
||||
Exit;
|
||||
end;
|
||||
{$ELSE}
|
||||
begin
|
||||
{$ENDIF}
|
||||
Paint(1);
|
||||
end;
|
||||
|
||||
procedure After;
|
||||
begin
|
||||
Finish(1);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const ASSEMBLER = `unit Assembler;
|
||||
|
||||
interface
|
||||
|
||||
function Log2Of(const V: Double): Double;
|
||||
function DivMod10(var Dividend: Cardinal): Cardinal;
|
||||
procedure Fill(P: Pointer);
|
||||
procedure After;
|
||||
|
||||
implementation
|
||||
|
||||
procedure Fill(P: Pointer);
|
||||
{$IFDEF FPC}nostackframe; assembler; asm {$ELSE} asm .noframe {$ENDIF}
|
||||
mov eax, 1
|
||||
end;
|
||||
|
||||
function Log2Of(const V: Double): Double;
|
||||
{$IFDEF WIN32_ASM}
|
||||
asm
|
||||
fld1
|
||||
fld V
|
||||
fyl2x
|
||||
{$ELSE}
|
||||
begin
|
||||
Result := Log2(V);
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
function DivMod10(var Dividend: Cardinal): Cardinal;
|
||||
{$IFNDEF WIN32_ASM}
|
||||
var
|
||||
Divided: Cardinal;
|
||||
begin
|
||||
Divided := Dividend div 10;
|
||||
Result := Remainder(Dividend, Divided);
|
||||
{$ELSE}
|
||||
const
|
||||
C10: Cardinal = 10;
|
||||
asm
|
||||
mov ecx, eax
|
||||
div C10
|
||||
{$ENDIF}
|
||||
end;
|
||||
|
||||
procedure After;
|
||||
begin
|
||||
Finish(1);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const RERAISES = `unit Reraises;
|
||||
|
||||
interface
|
||||
|
||||
procedure Load;
|
||||
procedure Parse;
|
||||
|
||||
implementation
|
||||
|
||||
procedure Load;
|
||||
begin
|
||||
try
|
||||
Read(1);
|
||||
except
|
||||
Log(1);
|
||||
raise;
|
||||
end
|
||||
end;
|
||||
|
||||
procedure Parse;
|
||||
begin
|
||||
try
|
||||
Scan(1);
|
||||
except
|
||||
on E: EConvertError do
|
||||
Recover(1);
|
||||
else raise
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const RECORDS = `unit Records;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TClient = class
|
||||
private
|
||||
fIn: record
|
||||
Headers: Pointer;
|
||||
URL, Method: string;
|
||||
end;
|
||||
fRows: array of packed record
|
||||
Id: Integer;
|
||||
end;
|
||||
fCount: Integer;
|
||||
public
|
||||
procedure Send;
|
||||
end;
|
||||
|
||||
TCaches = array[1..4] of record
|
||||
Size: Integer;
|
||||
end;
|
||||
|
||||
TState = record
|
||||
Seed: Integer;
|
||||
Data: record
|
||||
case Integer of
|
||||
0: (Lc: Pointer)
|
||||
end
|
||||
end;
|
||||
|
||||
var
|
||||
PosixInject: record
|
||||
GetSmbios: function(Index: Integer): string;
|
||||
OnIdle: procedure of object;
|
||||
end;
|
||||
Ready: Boolean;
|
||||
|
||||
const
|
||||
Origin: record X, Y: Integer; end = (X: 0; Y: 0);
|
||||
|
||||
implementation
|
||||
|
||||
procedure TClient.Send;
|
||||
begin
|
||||
Post(fIn.URL);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const LABELS = `unit Labels;
|
||||
|
||||
interface
|
||||
|
||||
function Find(P: PChar): Integer;
|
||||
procedure After;
|
||||
|
||||
implementation
|
||||
|
||||
function Find(P: PChar): Integer;
|
||||
label
|
||||
fnd, quit;
|
||||
begin
|
||||
Result := 0;
|
||||
if P = nil then
|
||||
goto quit;
|
||||
if Match(P) then
|
||||
goto fnd;
|
||||
goto quit;
|
||||
fnd:
|
||||
Result := 1;
|
||||
quit:
|
||||
end;
|
||||
|
||||
procedure After;
|
||||
begin
|
||||
Finish(1);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
const ALTERNATIVES = `unit Alternatives;
|
||||
|
||||
interface
|
||||
|
||||
function Repeated(Op: Integer): Integer;
|
||||
function Pick(Lookup: Pointer): Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function Repeated(Op: Integer): Integer;
|
||||
begin
|
||||
Result := 0;
|
||||
case Op of
|
||||
1:
|
||||
{$IFDEF UNICODE}
|
||||
begin
|
||||
CountWide(Result);
|
||||
end;
|
||||
{$ELSE}
|
||||
while Result < 10 do
|
||||
CountAnsi(Result);
|
||||
{$ENDIF}
|
||||
2:
|
||||
Result := 2;
|
||||
end;
|
||||
end;
|
||||
|
||||
function Pick(Lookup: Pointer): Integer;
|
||||
begin
|
||||
if Lookup = nil then
|
||||
{$IFDEF ASMX86}
|
||||
Result := PosAsm(0)
|
||||
{$ELSE}
|
||||
Result := PosPas(0)
|
||||
{$ENDIF}
|
||||
else
|
||||
Result := PosLookup(Lookup);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
|
||||
describe('Delphi syntax the Pascal grammar has no rule for', () => {
|
||||
it.each([
|
||||
['re-published properties', OVERRIDES],
|
||||
['conditionals after routine headers', HEADERS],
|
||||
['branches that open the same begin', BEGINS],
|
||||
['an assembler and a Pascal version of a body', ASSEMBLER],
|
||||
['re-raises', RERAISES],
|
||||
['anonymous records', RECORDS],
|
||||
['a label that ends a block', LABELS],
|
||||
['alternative statements where one goes', ALTERNATIVES],
|
||||
])('parses a unit whole past %s', (_name, code) => {
|
||||
expect(parsesWhole(code)).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps a class whole past its re-published properties', () => {
|
||||
const { calls, names } = extract(OVERRIDES, 'Marks.pas');
|
||||
expect(names).toEqual(expect.arrayContaining([
|
||||
'TMarkList::GetItem', 'TMarkList::Create', 'TMarkList::Items', 'TEditor::Lines', 'TEditor::Paint',
|
||||
]));
|
||||
// An override declares nothing new.
|
||||
expect(names).not.toContain('TMarkList::OwnsObjects');
|
||||
expect(names).not.toContain('TEditor::Align');
|
||||
expect(calls).toEqual(['Paint->Refresh']);
|
||||
});
|
||||
|
||||
it('gives each routine its calls past a conditional after its header', () => {
|
||||
expect(extract(HEADERS, 'Headers.pas').calls.sort()).toEqual(['Generate->Emit', 'KeyUp->Translate', 'Size->Measure']);
|
||||
expect(extract(BEGINS, 'Begins.pas').calls.sort()).toEqual(['After->Finish', 'Click->Fold', 'Click->Paint']);
|
||||
expect(extract(ASSEMBLER, 'Assembler.pas').calls.sort()).toEqual(['After->Finish', 'DivMod10->Remainder', 'Log2Of->Log2']);
|
||||
});
|
||||
|
||||
it('gives each routine its calls past a re-raise and a label that ends a block', () => {
|
||||
expect(extract(RERAISES, 'Reraises.pas').calls.sort()).toEqual(['Load->Log', 'Load->Read', 'Parse->Recover', 'Parse->Scan']);
|
||||
expect(extract(LABELS, 'Labels.pas').calls.sort()).toEqual(['After->Finish', 'Find->Match']);
|
||||
});
|
||||
|
||||
it('keeps a declaration of an anonymous record type, and every declaration after it', () => {
|
||||
const { calls, names } = extract(RECORDS, 'Records.pas');
|
||||
expect(names).toEqual(expect.arrayContaining([
|
||||
'TClient::fIn', 'TClient::fRows', 'TClient::fCount', 'TClient::Send', 'TCaches', 'TState::Seed', 'TState::Data',
|
||||
'PosixInject', 'Ready', 'Origin',
|
||||
]));
|
||||
// The record's own fields are no members of the class around it.
|
||||
expect(names).not.toContain('TClient::URL');
|
||||
expect(names).not.toContain('GetSmbios');
|
||||
expect(calls).toEqual(['Send->Post']);
|
||||
});
|
||||
|
||||
it('reads alternative statements where one goes as one block, keeping the calls of each', () => {
|
||||
expect(extract(ALTERNATIVES, 'Alternatives.pas').calls.sort()).toEqual([
|
||||
'Pick->PosAsm', 'Pick->PosLookup', 'Pick->PosPas', 'Repeated->CountAnsi', 'Repeated->CountWide',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps every offset and line, CRLF included', () => {
|
||||
for (const code of [OVERRIDES, HEADERS, BEGINS, ASSEMBLER, RERAISES, RECORDS, LABELS, ALTERNATIVES]) {
|
||||
const crlf = code.replace(/\n/g, '\r\n');
|
||||
const out = preParsePascal(crlf);
|
||||
expect(out.length).toBe(crlf.length);
|
||||
const breaks = (s: string) => [...s.matchAll(/\r\n/g)].map((m) => m.index);
|
||||
expect(breaks(out)).toEqual(breaks(crlf));
|
||||
}
|
||||
});
|
||||
|
||||
it('writes the words it needs where the grammar reads them', () => {
|
||||
const blank = (s: string) => s.replace(/[^\r\n]/g, ' ');
|
||||
expect(preParsePascal(' 1: {$IFDEF U} A(1); {$ELSE} B(2); {$ENDIF}\n'))
|
||||
.toBe(' 1: begin A(1); ; B(2); end; \n');
|
||||
expect(preParsePascal(' Data: record X: Integer; end;\n')).toBe(` Data: string${blank(' X: Integer; end')};\n`);
|
||||
expect(preParsePascal(' Data: packed record X: Integer; end end;\n')).toBe(` Data: string${blank(' record X: Integer; ')}; end;\n`);
|
||||
expect(preParsePascal(' else raise\n end;\n')).toBe(' else ; \n end;\n');
|
||||
});
|
||||
|
||||
it('leaves alone what the grammar reads as it is', () => {
|
||||
const code = `unit Keep;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TList<T: record> = class
|
||||
property Items[Index: Integer]: T read GetItem; default;
|
||||
property Count: Integer read FCount;
|
||||
Value: {$IFDEF CPU64} Int64 {$ELSE} Integer {$ENDIF};
|
||||
end;
|
||||
TRec = record
|
||||
X: Integer;
|
||||
end;
|
||||
|
||||
function Load: Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function Load: Integer;
|
||||
{$IFDEF PUREPASCAL}
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
Result := Read(I);
|
||||
end;
|
||||
{$ELSE}
|
||||
asm
|
||||
mov eax, 1
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
procedure Run;
|
||||
begin
|
||||
with List do
|
||||
{$IFDEF CPU64}
|
||||
if A = B then
|
||||
{$ELSE}
|
||||
if C = D then
|
||||
{$ENDIF}
|
||||
Exit;
|
||||
if Ready then {$IFDEF X} Go(1); {$ELSE} Stay(1); {$ENDIF}
|
||||
raise EAbort.Create('stop');
|
||||
WriteLn('raise; property X; quit:');
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
expect(preParsePascal(code)).toBe(code);
|
||||
});
|
||||
|
||||
it('links a routine past a conditional after its header to what it calls', async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-pascal-delphi-syntax-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'Lib.pas'), `unit Lib;
|
||||
|
||||
interface
|
||||
|
||||
procedure Translate(N: Integer);
|
||||
procedure Emit(N: Integer);
|
||||
|
||||
implementation
|
||||
|
||||
procedure Translate(N: Integer); begin end;
|
||||
procedure Emit(N: Integer); begin end;
|
||||
|
||||
end.
|
||||
`);
|
||||
fs.writeFileSync(path.join(dir, 'Headers.pas'), HEADERS.replace('implementation\n', 'implementation\n\nuses Lib;\n'));
|
||||
const cg = await CodeGraph.init(dir, { index: true });
|
||||
try {
|
||||
const callers = (name: string) => {
|
||||
const target = cg.getNodesByKind('function').find((n) => n.qualifiedName === name && n.filePath === 'Lib.pas');
|
||||
return target ? cg.getIncomingEdges(target.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.source)?.name) : [];
|
||||
};
|
||||
expect(callers('Translate')).toEqual(['KeyUp']);
|
||||
expect(callers('Emit')).toEqual(['Generate']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -360,4 +360,42 @@ end;`);
|
||||
expect(callsFrom(cg, 'THost::Fire')).toEqual(['Host.pas:THost::Fire::Calc', 'Host.pas:THost::Fire::Delete']);
|
||||
});
|
||||
});
|
||||
|
||||
it('gives a nested routine the constants it reads, not those its routine\'s body reads after it', async () => {
|
||||
const code = `unit Values;
|
||||
|
||||
interface
|
||||
|
||||
const
|
||||
MAX_DEPTH = 10;
|
||||
OP_EXACTLY = 3;
|
||||
OP_ANY = 4;
|
||||
|
||||
function Parse(N: Integer): Integer;
|
||||
|
||||
implementation
|
||||
|
||||
function Parse(N: Integer): Integer;
|
||||
|
||||
function Unquote(C: Integer): Integer;
|
||||
begin
|
||||
Result := C + OP_ANY;
|
||||
end;
|
||||
|
||||
begin
|
||||
if N > MAX_DEPTH then
|
||||
Result := OP_EXACTLY
|
||||
else
|
||||
Result := Unquote(N);
|
||||
end;
|
||||
|
||||
end.
|
||||
`;
|
||||
await withIndex({ 'Values.pas': code }, (cg) => {
|
||||
const reads = cg.getOutgoingEdges(routine(cg, 'Parse::Unquote').id)
|
||||
.filter((e) => e.kind === 'references')
|
||||
.map((e) => cg.getNode(e.target)!.name);
|
||||
expect(reads).toEqual(['OP_ANY']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -483,7 +483,7 @@ end.
|
||||
});
|
||||
|
||||
it('sees the declarations of a used unit whose header the parser lost', async () => {
|
||||
// The `{$IFDEF}` around a local `var` section costs the parse its `unit`
|
||||
// An old-style `begin … end.` initialization costs the parse its `unit`
|
||||
// node; the unit is still the one its file is named after.
|
||||
const lib = `unit Lib;
|
||||
|
||||
@@ -500,14 +500,12 @@ function FileSize(const FileName: string): Int64;
|
||||
implementation
|
||||
|
||||
function FileSize(const FileName: string): Int64;
|
||||
{$IFDEF MSWINDOWS}
|
||||
var
|
||||
Handle: THandle;
|
||||
{$ENDIF}
|
||||
begin
|
||||
Result := 0;
|
||||
end;
|
||||
|
||||
begin
|
||||
crc32c := nil;
|
||||
end.
|
||||
`;
|
||||
const main = `unit Main;
|
||||
@@ -606,16 +604,18 @@ end;
|
||||
});
|
||||
|
||||
it('takes no member of a class such a unit\'s parse lost for a routine of the unit', async () => {
|
||||
// The anonymous record ends the class early, so `Insert` comes out as a
|
||||
// routine of the unit, indented as no routine of the unit is.
|
||||
// The anonymous record holding a method, which the parser can't read,
|
||||
// ends the class early, so `Insert` comes out as a routine of the unit,
|
||||
// indented as no routine of the unit is.
|
||||
const lib = `unit Lib;
|
||||
|
||||
interface
|
||||
|
||||
type
|
||||
TClient = class
|
||||
Items: array of record
|
||||
Items: record
|
||||
Name: string;
|
||||
procedure Clear;
|
||||
end;
|
||||
procedure Insert(Index: Integer; const S: string);
|
||||
end;
|
||||
@@ -627,13 +627,10 @@ implementation
|
||||
procedure TClient.Insert(Index: Integer; const S: string); begin end;
|
||||
|
||||
procedure Log(const S: string);
|
||||
{$IFDEF MSWINDOWS}
|
||||
var
|
||||
Handle: THandle;
|
||||
{$ENDIF}
|
||||
begin
|
||||
end;
|
||||
|
||||
begin
|
||||
end.
|
||||
`;
|
||||
const main = `unit Main;
|
||||
|
||||
Reference in New Issue
Block a user