program semicolonless;
{$mode objfpc}{$H+}
{$MODESWITCH AdvancedRecords}
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Classes
{ you can add units after this },
PScanner,
SysUtils;
type
TPascalScannerIgnoreDirectives = class; // this ending semicolon is necessary
var
outTokens: array of record
TokenKind: TToken;
TokenString: string;
end;
outTokenCount: integer;
procedure AddToOutput(ATokenType: TToken; ATokenString: string); forward;
procedure InsertIntoOutput(AIndex: integer; ATokenType: TToken; ATokenString: string); forward;
const
TokensWhereSemicolonIndicatesEmptyStatement = [tkColon, tkdo, tkelse, tkthen{, tkOtherwise}];
TokensNeutrallyFollowedBySemicolon =
[tkIdentifier, tkString, tkNumber, tkChar, tkBraceClose, tkSquaredBraceClose, tkCaret,
tkdispinterface, tkend, tkfalse, tkfile, tkinherited, tkinline, tkinterface,
tknil, tkobject, tkself, tkthreadvar, tktrue]; { - private protected public published otherwise }
TokensNotIndicatingStatementContinuation =
[tkWhitespace, tkComment, tkIdentifier, tkString, tkNumber, tkChar,
tkBraceOpen, tkAt, tkasm, tkbegin, tkcase, tkconst, tkend, tkexports,
tkfinalization, tkfor, tkfunction, tkgoto, tkif, tkimplementation, tkinherited,
tkinitialization, tklabel, tklibrary, tkoperator, tkprocedure, tkprogram, tkproperty,
tkraise, tkrepeat, tkResourceString, tkthreadvar, tktry, tktype, tkunit, tkuntil,
tkuses, tkvar, tkwhile, tkwith];
type
{ TPascalScannerIgnoreDirectives }
TPascalScannerIgnoreDirectives = class(TPascalScanner)
protected
function HandleDirective(const {%H-}ADirectiveText: String): TToken; override;
end;
function TPascalScannerIgnoreDirectives.HandleDirective(const ADirectiveText: String): TToken;
begin
Result:= tkComment; // treat directives as comments
end;
{********************** Output *********************}
procedure AddToOutput(ATokenType: TToken; ATokenString: string);
begin
if outTokenCount >= length(outTokens) then
SetLength(outTokens, outTokenCount * 2 + 4);
outTokens[outTokenCount].TokenKind:= ATokenType;
outTokens[outTokenCount].TokenString:= ATokenString;
inc(outTokenCount);
end;
procedure InsertIntoOutput(AIndex: integer; ATokenType: TToken; ATokenString: string);
var
i: Integer;
begin
if (AIndex < 0) or (AIndex > outTokenCount) then raise exception.Create('Index out of bounds');
if outTokenCount >= length(outTokens) then
SetLength(outTokens, outTokenCount * 2 + 4);
for i := outTokenCount-1 downto AIndex do
outTokens[i+1] := outTokens[i];
outTokens[AIndex].TokenKind:= ATokenType;
outTokens[AIndex].TokenString:= ATokenString;
inc(outTokenCount);
end;
procedure ReadInput(AInputName: string);
var
resolver: TFileResolver;
scanner: TPascalScanner;
curToken: TToken;
curColStart, curColEnd: integer;
curInputLine: string;
procedure FetchNextToken;
var
inputRow: Integer;
begin
inputRow := scanner.CurRow;
curColStart := scanner.CurColumn;
curInputLine := scanner.CurLine;
curToken := scanner.FetchToken;
if scanner.CurRow > inputRow then
curColEnd := length(curInputLine)
else curColEnd:= scanner.CurColumn;
end;
begin
outTokens := nil;
outTokenCount := 0;
resolver := TFileResolver.Create;
scanner := TPascalScannerIgnoreDirectives.Create(resolver);
try
scanner.OpenFile(AInputName);
FetchNextToken;
while curToken <> tkEOF do
begin
case curToken of
tkWhitespace, tkComment: AddToOutput(curToken,
copy(curInputLine, curColStart + 1, curColEnd - curColStart));
tkIdentifier..tkChar: AddToOutput(curToken, scanner.CurTokenString);
tkTab: AddToOutput(tkTab, #9); (* keep tabs *)
tkLineEnding: AddToOutput(tkLineEnding, LineEnding);
otherwise
AddToOutput(curToken, TokenInfos[curToken]);
end;
FetchNextToken;
end;
finally
scanner.Free;
resolver.Free;
end;
end;
procedure WriteOutput(AOutputName: string);
var
i: Integer;
fout: TextFile;
begin
assignfile(fout, AOutputName);
Rewrite(fout);
for i := 0 to outTokenCount-1 do
write(fout, outTokens[i].TokenString);
closefile(fout);
end;
{******************** Processing ********************}
procedure RemoveSemicolonsInOutput;
var
i, j: Integer;
prevToken, nextToken, nextLineToken: TToken;
nextTokenPos: integer;
prevTokenStr: String;
begin
for i := 0 to outTokenCount-1 do
if (outTokens[i].TokenKind = tkSemicolon) then
begin
nextToken := tkLineEnding;
nextTokenPos:= i;
for j := i+1 to outTokenCount-1 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace] then
continue else
begin
nextToken := outTokens[j].TokenKind;
nextTokenPos := j;
break;
end;
// found a semicolon that might be removed
if nextToken = tkLineEnding then
begin
nextLineToken := tkEOF;
for j := nextTokenPos+1 to outTokenCount-1 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace, tkLineEnding] then
continue else
begin
nextLineToken:= outTokens[j].TokenKind;
break;
end;
if nextLineToken in TokensNotIndicatingStatementContinuation then
begin
prevToken := tkLineEnding;
prevTokenStr := '';
for j := i-1 downto 0 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace, tkLineEnding]
then continue else
begin
prevToken := outTokens[j].TokenKind;
prevTokenStr := outTokens[j].TokenString;
break;
end;
if (prevToken in TokensWhereSemicolonIndicatesEmptyStatement) or
(compareText(prevTokenStr, 'otherwise') = 0) then
begin
outTokens[i].TokenKind:= tknil;
outTokens[i].TokenString:= 'nil';
end
else if (prevToken in TokensNeutrallyFollowedBySemicolon) and
(compareText(prevTokenStr, 'otherwise') <> 0) and
(compareText(prevTokenStr, 'private') <> 0) and
(compareText(prevTokenStr, 'protected') <> 0) and
(compareText(prevTokenStr, 'public') <> 0) and
(compareText(prevTokenStr, 'published') <> 0) then
begin
outTokens[i].TokenKind:= tkWhitespace;
outTokens[i].TokenString:= '';
end;
end;
end;
end;
end;
procedure RemoveSemicolons(AInputName, AOutputName : string);
begin
ReadInput(AInputName);
RemoveSemicolonsInOutput;
WriteOutput(AOutputName);
end;
procedure AddSemicolonsInOutput;
var
i, j, prevTokenPos: Integer;
prevToken, prevToken2, nextToken, classToken: TToken;
prevTokenStr, prevTokenStr2: String;
forFound: Boolean;
begin
for i := outTokenCount downto 0 do
if (i = outTokenCount) or (outTokens[i].TokenKind = tkLineEnding) then
begin
prevToken := tkLineEnding;
prevTokenPos := i;
for j := i-1 downto 0 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace] then
continue else
begin
prevToken := outTokens[j].TokenKind;
prevTokenPos := j;
prevTokenStr := outTokens[j].TokenString;
break;
end;
if prevToken = tknil then
begin
prevToken2 := tkLineEnding;
prevTokenStr2 := '';
for j := prevTokenPos-1 downto 0 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace, tkLineEnding] then
continue else
begin
prevToken2 := outTokens[j].TokenKind;
prevTokenStr2 := outTokens[j].TokenString;
break;
end;
if (prevToken2 in TokensWhereSemicolonIndicatesEmptyStatement) or
(CompareText(prevTokenStr2, 'otherwise')=0) then
begin
outTokens[prevTokenPos].TokenKind := tkSemicolon;
outTokens[prevTokenPos].TokenString:= ';';
continue;
end;
end;
if (prevToken in TokensNeutrallyFollowedBySemicolon) and
(compareText(prevTokenStr, 'otherwise') <> 0) and
(compareText(prevTokenStr, 'private') <> 0) and
(compareText(prevTokenStr, 'protected') <> 0) and
(compareText(prevTokenStr, 'public') <> 0) and
(compareText(prevTokenStr, 'published') <> 0) then
begin
// class|record|type helper for T
if prevToken = tkIdentifier then
begin
forFound := false;
for j := prevTokenPos-1 downto 0 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace, tkLineEnding,
tkDot, tkIdentifier] then
continue else
if outTokens[j].TokenKind = tkfor then
begin
forFound := true;
break;
end else break;
if forFound then continue;
end;
// type class|object|interface|dispinterface(parent1...)
if prevToken = tkBraceClose then
begin
classToken:= tkLineEnding;
for j := prevTokenPos-1 downto 0 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace, tkLineEnding,
tkDot, tkIdentifier, tkComma,
tkGreaterThan, tkLessThan, tkspecialize] then
continue else
if outTokens[j].TokenKind = tkBraceOpen then
begin
if j > 0 then classToken := outTokens[j-1].TokenKind;
break;
end else break;
if classToken in[tkclass, tkobject, tkinterface, tkdispinterface] then continue;
end;
nextToken := tkLineEnding;
for j := i+1 to outTokenCount-1 do
if outTokens[j].TokenKind in [tkComment, tkTab, tkWhiteSpace, tkLineEnding] then
continue else
begin
nextToken := outTokens[j].TokenKind;
break;
end;
if nextToken in TokensNotIndicatingStatementContinuation then
InsertIntoOutput(prevTokenPos+1, tkSemicolon, ';');
end;
end;
end;
procedure AddSemicolons(AInputName, AOutputName : string);
begin
ReadInput(AInputName);
AddSemicolonsInOutput;
WriteOutput(AOutputName);
end;
procedure FixSemicolons(AInputName, AOutputName : string);
begin
ReadInput(AInputName);
RemoveSemicolonsInOutput;
AddSemicolonsInOutput;
WriteOutput(AOutputName);
end;
{********************** Testing *********************}
type
{ TPointHelper }
TPointHelper = record helper for TPoint
function Sum: integer;
end;
function TPointHelper.Sum: integer;
begin
result := x + y;
end;
procedure TestStatements;
var c: char;
begin
write('hihi');;
//test empty statement
if true then ;
//test case else
case random(4) of
1: ;
7: if true then write('haha');
else write('hoho');
end;
c := 'A';
case c of
'B': ;
'C': if true then write('haha');
otherwise
;
end;
end;
{******************* Main program *******************}
var
sourceFile, targetFile, mode: string;
begin
if ParamCount < 2 then
begin
writeln('Usage: semicolonless -L|-M|-F <input file> [<output file>]');
writeln;
writeln('-L : less semicolons (default extension .less)');
writeln('-M : more semicolons (default extension .pas)');
writeln('-F : fix semicolons');
halt;
end;
mode := paramstr(1);
if copy(mode,1,1) <> '-' then
begin
writeln('Expecting mode');
halt;
end;
sourceFile := paramstr(2);
if not FileExists(sourceFile) then
begin
writeln('Input file not found');
halt;
end;
if ParamCount >= 3 then
targetFile := paramStr(3)
else targetFile := '';
if mode = '-L' then
begin
if targetFile = '' then targetFile := ChangeFileExt(sourceFile,'.less') else
if ExtractFileExt(targetFile) = '' then targetFile += '.less';
RemoveSemicolons(sourceFile, targetFile);
end else
if mode = '-M' then
begin
if targetFile = '' then targetFile := ChangeFileExt(sourceFile, '.pas') else
if ExtractFileExt(targetFile) = '' then targetFile += '.pas';
AddSemicolons(sourceFile, targetFile);
end else
if mode = '-F' then
begin
if targetFile = '' then targetFile := sourceFile;
FixSemicolons(sourceFile, targetFile);
end else
writeln('Unknown mode');
end.