Browse Source

pastojs: uses-in filename

git-svn-id: trunk@38324 -
Mattias Gaertner 7 years ago
parent
commit
80d8a73f05

+ 1 - 0
.gitattributes

@@ -6897,6 +6897,7 @@ packages/pastojs/tests/tcfiler.pas svneol=native#text/plain
 packages/pastojs/tests/tcmodules.pas svneol=native#text/plain
 packages/pastojs/tests/tcoptimizations.pas svneol=native#text/plain
 packages/pastojs/tests/tcsrcmap.pas svneol=native#text/plain
+packages/pastojs/tests/tcunitsearch.pas svneol=native#text/plain
 packages/pastojs/tests/testpas2js.lpi svneol=native#text/plain
 packages/pastojs/tests/testpas2js.pp svneol=native#text/plain
 packages/paszlib/Makefile svneol=native#text/plain

+ 137 - 49
packages/pastojs/src/pas2jscompiler.pp

@@ -1,4 +1,4 @@
-{ Author: Mattias Gaertner  2017  [email protected]
+{ Author: Mattias Gaertner  2018  [email protected]
 
 Abstract:
   TPas2jsCompiler is the wheel boss of the pas2js compiler.
@@ -253,8 +253,10 @@ type
     constructor Create(aCompiler: TPas2jsCompiler; const aPasFilename: string);
     destructor Destroy; override;
     procedure CreateScannerAndParser(aFileResolver: TPas2jsFileResolver);
-    function OnPasTreeFindModule(const UseUnitname: String): TPasModule;
-    function FindUnit(const UseUnitname: String): TPasModule;
+    function OnPasTreeFindModule(const UseUnitName, InFilename: String; NameExpr,
+      InFileExpr: TPasExpr): TPasModule;
+    function FindUnit(const UseUnitname, InFilename: String;
+      NameExpr, InFileExpr: TPasExpr): TPasModule;
     procedure OnPasTreeCheckSrcName(const Element: TPasElement);
     procedure OpenFile(aFilename: string);// beware: this changes FileResolver.BaseDirectory
     procedure ParsePascal;
@@ -381,6 +383,8 @@ type
     procedure InitParamMacros;
     procedure ClearDefines;
     procedure RaiseInternalError(id: int64; Msg: string);
+    function GetExitCode: Longint; virtual;
+    procedure SetExitCode(Value: Longint); virtual;
   public
     constructor Create; virtual;
     destructor Destroy; override;
@@ -413,6 +417,8 @@ type
     procedure LoadPasFile(PasFilename, UseUnitName: string; out aFile: TPas2jsCompilerFile);
     function FindUsedUnit(const TheUnitName: string): TPas2jsCompilerFile;
     procedure AddUsedUnit(aFile: TPas2jsCompilerFile);
+
+    function DirectoryExists(const Filename: string): boolean;
   public
     property CompilerExe: string read FCompilerExe write SetCompilerExe;
     property ConditionEvaluator: TCondDirectiveEvaluator read FConditionEval;
@@ -443,6 +449,7 @@ type
     property TargetPlatform: TPasToJsPlatform read FTargetPlatform write SetTargetPlatform;
     property TargetProcessor: TPasToJsProcessor read FTargetProcessor write SetTargetProcessor;
     property WPOAnalyzer: TPas2JSWPOptimizer read FWPOAnalyzer; // Whole Program Optimization
+    property ExitCode: longint read GetExitCode write SetExitCode;
   end;
 
 function CompareCompilerFilesPasFile(Item1, Item2: Pointer): integer;
@@ -806,6 +813,8 @@ begin
   try
     Parser.ParseContinueImplementation;
   except
+    on E: ECompilerTerminate do
+      raise;
     on E: Exception do
       HandleException(E);
   end;
@@ -929,8 +938,10 @@ end;
 procedure TPas2jsCompilerFile.HandleUnknownException(E: Exception);
 begin
   if not (E is ECompilerTerminate) then
-    Log.Log(mtFatal,E.ClassName+': '+E.Message,0);
+    Log.Log(mtFatal,'bug: uncaught ECompilerTerminate'+': '+E.Message,0); // must use on E:ECompilerTerminate do raise;
+  Log.Log(mtFatal,E.ClassName+': '+E.Message,0);
   Compiler.Terminate(ExitCodeErrorInternal);
+  // Note: a "raise E" is not allowed by caught exceptions, try..except will free it
 end;
 
 procedure TPas2jsCompilerFile.HandleException(E: Exception);
@@ -995,6 +1006,8 @@ begin
     // analyze
     UseAnalyzer.AnalyzeModule(FPasModule);
   except
+    on E: ECompilerTerminate do
+      raise;
     on E: Exception do
       HandleException(E);
   end;
@@ -1006,6 +1019,8 @@ begin
   try
     Scanner.OpenFile(PasFilename);
   except
+    on E: ECompilerTerminate do
+      raise;
     on E: Exception do
       HandleException(E);
   end;
@@ -1015,6 +1030,8 @@ procedure TPas2jsCompilerFile.ParsePascal;
 begin
   if ShowDebug then
     Log.LogPlain(['Debug: Parsing Pascal "',PasFilename,'"...']);
+  if FPasModule<>nil then
+    raise ECompilerTerminate.Create('TPas2jsCompilerFile.ParsePascal '+PasFilename);
   try
     // parse Pascal
     PascalResolver.InterfaceOnly:=IsForeign;
@@ -1029,6 +1046,8 @@ begin
       exit;
     ParserFinished;
   except
+    on E: ECompilerTerminate do
+      raise;
     on E: Exception do
       HandleException(E);
   end;
@@ -1054,6 +1073,8 @@ begin
     FConverter.OnIsTypeInfoUsed:=@OnConverterIsTypeInfoUsed;
     FJSModule:=Converter.ConvertPasElement(PasModule,PascalResolver);
   except
+    on E: ECompilerTerminate do
+      raise;
     on E: Exception do
       HandleException(E);
   end;
@@ -1165,7 +1186,8 @@ begin
   end;
 end;
 
-function TPas2jsCompilerFile.OnPasTreeFindModule(const UseUnitname: String): TPasModule;
+function TPas2jsCompilerFile.OnPasTreeFindModule(const UseUnitName,
+  InFilename: String; NameExpr, InFileExpr: TPasExpr): TPasModule;
 var
   aNameSpace: String;
   LastEl: TPasElement;
@@ -1186,14 +1208,14 @@ begin
   else
     RaiseInternalError(20170504161408,'internal error TPas2jsCompilerFile.FindModule PasTree.LastElement='+GetObjName(LastEl)+' '+GetObjName(LastEl.Parent));
 
-  if (Pos('.',UseUnitname)<1) then
+  if (InFilename='') and (Pos('.',UseUnitname)<1) then
   begin
     // generic unit -> search with namespaces
     // first the default program namespace
     aNameSpace:=Compiler.GetDefaultNamespace;
     if aNameSpace<>'' then
     begin
-      Result:=FindUnit(aNameSpace+'.'+UseUnitname);
+      Result:=FindUnit(aNameSpace+'.'+UseUnitname,'',nil,nil);
       if Result<>nil then exit;
     end;
 
@@ -1201,17 +1223,18 @@ begin
     for i:=0 to Compiler.FileCache.Namespaces.Count-1 do begin
       aNameSpace:=Compiler.FileCache.Namespaces[i];
       if aNameSpace='' then continue;
-      Result:=FindUnit(aNameSpace+'.'+UseUnitname);
+      Result:=FindUnit(aNameSpace+'.'+UseUnitname,'',nil,nil);
       if Result<>nil then exit;
     end
   end;
 
   // search in unitpath
-  Result:=FindUnit(UseUnitname);
-  // if nil resolver will give a nice error position
+  Result:=FindUnit(UseUnitname,InFilename,NameExpr,InFileExpr);
+  // if Result=nil resolver will give a nice error position
 end;
 
-function TPas2jsCompilerFile.FindUnit(const UseUnitname: String): TPasModule;
+function TPas2jsCompilerFile.FindUnit(const UseUnitname, InFilename: String;
+  NameExpr, InFileExpr: TPasExpr): TPasModule;
 
   function FindCycle(aFile, SearchFor: TPas2jsCompilerFile;
     var Cycle: TFPList): boolean;
@@ -1263,7 +1286,7 @@ var
             if i>0 then CyclePath+=',';
             CyclePath+=TPas2jsCompilerFile(Cycle[i]).GetModuleName;
           end;
-          Parser.RaiseParserError(nUnitCycle,[CyclePath]);
+          PascalResolver.RaiseMsg(20180223141537,nUnitCycle,sUnitCycle,[CyclePath],NameExpr);
         end;
       finally
         Cycle.Free;
@@ -1275,35 +1298,61 @@ var
   end;
 
 var
-  UsePasFilename, InFilename, UseJSFilename: String;
+  UsePasFilename, UseJSFilename, ActualUnitname: String;
   UseIsForeign: boolean;
+  OtherFile: TPas2jsCompilerFile;
 begin
   Result:=nil;
-  InFilename:='';
 
-  // first try registered units
-  aFile:=Compiler.FindUsedUnit(UseUnitname);
+  // first try loaded units
+  aFile:=nil;
+  if InFilename='' then
+    aFile:=Compiler.FindUsedUnit(UseUnitname);
+  UsePasFilename:='';
+  if aFile=nil then
+  begin
+    // search Pascal file
+    UsePasFilename:=FileResolver.FindUnitFileName(UseUnitname,InFilename,UseIsForeign);
+    if UsePasFilename='' then
+    begin
+      // can't find unit
+      exit;
+    end;
+  end;
+
+  ActualUnitname:=UseUnitname;
+  if InFilename<>'' then
+  begin
+    ActualUnitname:=ExtractFilenameOnly(InFilename);
+    aFile:=Compiler.FindPasFile(UsePasFilename);
+  end;
+
   if aFile<>nil then
   begin
     // known unit
-    if (aFile.PasUnitName<>'') and (CompareText(aFile.PasUnitName,UseUnitname)<>0) then
+    if (aFile.PasUnitName<>'') and (CompareText(aFile.PasUnitName,ActualUnitname)<>0) then
     begin
       Log.LogPlain(['Debug: TPas2jsPasTree.FindUnit unitname MISMATCH aFile.PasUnitname="',aFile.PasUnitName,'"',
          ' Self=',FileResolver.Cache.FormatPath(PasFilename),
-         ' Uses=',UseUnitname,
+         ' Uses=',ActualUnitname,
          ' IsForeign=',IsForeign]);
       RaiseInternalError(20170504161412,'TPas2jsPasTree.FindUnit unit name mismatch');
     end;
     CheckCycle;
   end else begin
-    // new unit -> search
+    // new unit
 
-    // search Pascal file
-    UsePasFilename:=FileResolver.FindUnitFileName(UseUnitname,InFilename,UseIsForeign);
-    if UsePasFilename='' then
+    if InFilename<>'' then
     begin
-      // can't find unit
-      exit;
+      aFile:=Compiler.FindUsedUnit(ActualUnitname);
+      if aFile<>nil then
+      begin
+        {$IF defined(VerbosePasResolver) or defined(VerbosePas2JS)}
+        writeln('TPas2jsCompilerFile.FindUnit in-file unit name duplicate: New=',UsePasFilename,' Old=',aFile.PasFilename);
+        {$ENDIF}
+        PascalResolver.RaiseMsg(20180223141323,nDuplicateFileFound,sDuplicateFileFound,
+          [UsePasFilename,aFile.PasFilename],InFileExpr);
+      end;
     end;
 
     UseJSFilename:='';
@@ -1314,23 +1363,40 @@ begin
     //    ' IsForeign=',IsForeign,' JSFile="',FileResolver.Cache.FormatPath(useJSFilename),'"']);
 
     // load Pascal file
-    Compiler.LoadPasFile(UsePasFilename,UseUnitname,aFile);
+    Compiler.LoadPasFile(UsePasFilename,ActualUnitname,aFile);
+
+    // consistency checks
+    if aFile.PasUnitName<>ActualUnitname then
+      RaiseInternalError(20170922143329,'aFile.PasUnitName='+aFile.PasUnitName+' UseUnitname='+ActualUnitname);
+    if CompareFilenames(aFile.PasFilename,UsePasFilename)<>0 then
+      RaiseInternalError(20170922143330,'aFile.PasFilename='+aFile.PasFilename+' UsePasFilename='+UsePasFilename);
+
     if aFile=Self then
     begin
       // unit uses itself -> cycle
-      Parser.RaiseParserError(nUnitCycle,[UseUnitname]);
+      Parser.RaiseParserError(nUnitCycle,[ActualUnitname]);
     end;
-    if aFile.PasUnitName<>UseUnitname then
-      RaiseInternalError(20170922143329,'aFile.PasUnitName='+aFile.PasUnitName+' UseUnitname='+UseUnitname);
 
+    // add file to trees
     Compiler.AddUsedUnit(aFile);
-    if aFile<>Compiler.FindUsedUnit(UseUnitname) then
-      begin
-      if Compiler.FindUsedUnit(UseUnitname)=nil then
-        RaiseInternalError(20170922143405,'UseUnitname='+UseUnitname)
+    // consistency checks
+    OtherFile:=Compiler.FindUsedUnit(ActualUnitname);
+    if aFile<>OtherFile then
+    begin
+      if OtherFile=nil then
+        RaiseInternalError(20170922143405,'ActualUnitname='+ActualUnitname)
       else
-        RaiseInternalError(20170922143511,'UseUnitname='+UseUnitname+' Found='+Compiler.FindUsedUnit(UseUnitname).PasUnitName);
-      end;
+        RaiseInternalError(20170922143511,'ActualUnitname='+ActualUnitname+' Found='+OtherFile.PasUnitName);
+    end;
+    OtherFile:=Compiler.FindPasFile(UsePasFilename);
+    if aFile<>OtherFile then
+    begin
+      if OtherFile=nil then
+        RaiseInternalError(20180224094625,'UsePasFilename='+UsePasFilename)
+      else
+        RaiseInternalError(20180224094627,'UsePasFilename='+UsePasFilename+' Found='+OtherFile.PasFilename);
+    end;
+
     CheckCycle;
 
     aFile.JSFilename:=UseJSFilename;
@@ -1706,7 +1772,6 @@ var
 var
   DestFilename, DestDir, Src, MapFilename: String;
   aJSWriter: TJSWriter;
-  fs: TFileStream;
   ms: TMemoryStream;
 begin
   //writeln('TPas2jsCompiler.WriteJSFiles ',aFile.PasFilename,' Need=',aFile.NeedBuild,' Checked=',Checked.Find(aFile)<>nil);
@@ -1791,24 +1856,26 @@ begin
 
       // write js
       try
-        fs:=TFileStream.Create(DestFilename,fmCreate);
+        ms:=TMemoryStream.Create;
         try
           // UTF8-BOM
           if (Log.Encoding='') or (Log.Encoding='utf8') then
           begin
             Src:=String(UTF8BOM);
-            fs.Write(Src[1],length(Src));
+            ms.Write(Src[1],length(Src));
           end;
           // JS source
-          fs.Write(aFileWriter.Buffer^,aFileWriter.BufferLength);
+          ms.Write(aFileWriter.Buffer^,aFileWriter.BufferLength);
           // source map comment
           if aFileWriter.SrcMap<>nil then
           begin
             Src:='//# sourceMappingURL='+ExtractFilename(MapFilename)+LineEnding;
-            fs.Write(Src[1],length(Src));
+            ms.Write(Src[1],length(Src));
           end;
+          ms.Position:=0;
+          FileCache.SaveToFile(ms,DestFilename);
         finally
-          fs.Free;
+          ms.Free;
         end;
       except
         on E: Exception do begin
@@ -1830,7 +1897,7 @@ begin
             // Note: No UTF-8 BOM in source map, Chrome 59 gives an error
             aFileWriter.SrcMap.SaveToStream(ms);
             ms.Position:=0;
-            ms.SaveToFile(MapFilename);
+            FileCache.SaveToFile(ms,MapFilename);
           finally
             ms.Free;
           end;
@@ -1899,6 +1966,16 @@ begin
   raise Exception.Create(Msg);
 end;
 
+function TPas2jsCompiler.GetExitCode: Longint;
+begin
+  Result:=System.ExitCode;
+end;
+
+procedure TPas2jsCompiler.SetExitCode(Value: Longint);
+begin
+  System.ExitCode:=Value;
+end;
+
 procedure TPas2jsCompiler.Terminate(TheExitCode: integer);
 begin
   ExitCode:=TheExitCode;
@@ -2152,7 +2229,7 @@ procedure TPas2jsCompiler.LoadDefaultConfig;
   begin
     Result:=false;
     if aFilename='' then exit;
-    aFilename:=ExpandFileNameUTF8(aFilename);
+    aFilename:=ExpandFileNameUTF8(aFilename,FileCache.BaseDirectory);
     if ShowTriedUsedFiles then
       Log.LogMsgIgnoreFilter(nConfigFileSearch,[aFilename]);
     if not DirectoryCache.FileExists(aFilename) then exit;
@@ -2217,6 +2294,7 @@ var
   Enable: Boolean;
   aPlatform: TPasToJsPlatform;
 begin
+  //writeln('TPas2jsCompiler.ReadParam ',Param,' ',Quick,' ',FromCmdLine);
   if ShowDebug then
     if Quick then
       Log.LogMsgIgnoreFilter(nQuickHandlingOption,[Param])
@@ -2587,7 +2665,7 @@ begin
       aFilename:=copy(Param,2,length(Param));
       if aFilename='' then
         ParamFatal('invalid config file at param position '+IntToStr(i));
-      aFilename:=ExpandFileNameUTF8(aFilename);
+      aFilename:=ExpandFileNameUTF8(aFilename,FileCache.BaseDirectory);
       if not DirectoryCache.FileExists(aFilename) then
         ParamFatal('config file not found: "'+copy(Param,2,length(Param))+'"');
       LoadConfig(aFilename);
@@ -2600,7 +2678,7 @@ begin
         CfgSyntaxError('invalid parameter');
       if FileCache.MainSrcFile<>'' then
         ParamFatal('Two Pascal files. Only one Pascal file is supported.');
-      aFilename:=ExpandFileNameUTF8(Param);
+      aFilename:=ExpandFileNameUTF8(Param,FileCache.BaseDirectory);
       if not DirectoryCache.FileExists(aFilename) then
         ParamFatal('Pascal file not found: "'+Param+'"');
       FileCache.MainSrcFile:=aFilename;
@@ -2817,7 +2895,7 @@ end;
 procedure TPas2jsCompiler.SetCompilerExe(AValue: string);
 begin
   if AValue<>'' then
-    AValue:=ExpandFileNameUTF8(AValue);
+    AValue:=ExpandFileNameUTF8(AValue,FileCache.BaseDirectory);
   if FCompilerExe=AValue then Exit;
   FCompilerExe:=AValue;
 end;
@@ -3090,9 +3168,11 @@ begin
   if FileCount>0 then
     RaiseInternalError(20170504161340,'internal error: TPas2jsCompiler.Run FileCount>0');
 
-  CompilerExe:=aCompilerExe;
+  // ste working directory, need by all relative filenames
   FileCache.BaseDirectory:=aWorkingDir;
 
+  CompilerExe:=aCompilerExe; // maybe needed to find the default config
+
   // quick check command line params
   for i:=0 to ParamList.Count-1 do
     ReadParam(ParamList[i],true,true);
@@ -3466,8 +3546,8 @@ begin
     Terminate(ExitCodeFileNotFound);
   end;
 
-  PasFilename:=ExpandFileNameUTF8(PasFilename);
-  if DirectoryExists(PasFilename) then
+  PasFilename:=ExpandFileNameUTF8(PasFilename,FileCache.BaseDirectory);
+  if DirectoryCache.DirectoryExists(PasFilename) then
   begin
     Log.LogMsg(nFileIsFolder,[PasFilename]);
     Terminate(ExitCodeFileNotFound);
@@ -3479,7 +3559,10 @@ begin
     {$IFDEF VerboseSetPasUnitName}
     writeln('TPas2jsCompiler.LoadPasFile File="',PasFilename,'" UseUnit="',UseUnitName,'"');
     {$ENDIF}
-    aFile.PasUnitName:=UseUnitName;
+    if CompareText(ExtractFilenameOnly(PasFilename),UseUnitName)=0 then
+      aFile.PasUnitName:=UseUnitName
+    else
+      aFile.PasUnitName:=ExtractFilenameOnly(PasFilename);
     end;
   FFiles.Add(aFile);
   aFile.ShowDebug:=ShowDebug;
@@ -3533,5 +3616,10 @@ begin
   end;
 end;
 
+function TPas2jsCompiler.DirectoryExists(const Filename: string): boolean;
+begin
+  Result:=FileCache.DirectoryCache.DirectoryExists(Filename);
+end;
+
 end.
 

+ 39 - 2
packages/pastojs/src/pas2jsfilecache.pp

@@ -243,6 +243,7 @@ type
   end;
 
   TPas2jsReadFileEvent = function(aFilename: string; var aSource: string): boolean of object;
+  TPas2jsWriteFileEvent = procedure(aFilename: string; Source: string) of object;
 
   TPas2jsCachedFilesState = (
     cfsMainJSFileResolved
@@ -273,6 +274,7 @@ type
     FNamespaces: TStringList;
     FNamespacesFromCmdLine: integer;
     FOnReadFile: TPas2jsReadFileEvent;
+    FOnWriteFile: TPas2jsWriteFileEvent;
     FOptions: TP2jsFileCacheOptions;
     FReadLineCounter: SizeInt;
     FResetStamp: TChangeStamp;
@@ -321,6 +323,7 @@ type
     procedure GetListing(const aDirectory: string; var Files: TStrings;
                          FullPaths: boolean = true);
     procedure RaiseDuplicateFile(aFilename: string);
+    procedure SaveToFile(ms: TMemoryStream; Filename: string);
   public
     property AllJSIntoMainJS: Boolean read GetAllJSIntoMainJS write SetAllJSIntoMainJS;
     property BaseDirectory: string read FBaseDirectory write SetBaseDirectory; // includes trailing pathdelim
@@ -346,6 +349,7 @@ type
     property UnitPaths: TStringList read FUnitPaths;
     property UnitPathsFromCmdLine: integer read FUnitPathsFromCmdLine;
     property OnReadFile: TPas2jsReadFileEvent read FOnReadFile write FOnReadFile;
+    property OnWriteFile: TPas2jsWriteFileEvent read FOnWriteFile write FOnWriteFile;
   end;
 
 function CompareFilenameWithCachedFile(Filename, CachedFile: Pointer): integer;
@@ -1293,13 +1297,24 @@ function TPas2jsFileResolver.FindUnitFileName(const aUnitname,
 
 var
   i: Integer;
+  aFilename: String;
 begin
   Result:='';
+  IsForeign:=false;
 
   if InFilename<>'' then
   begin
-    Cache.Log.LogMsgIgnoreFilter(nSearchingFileNotFound,['not yet implemented "in" '+Cache.FormatPath(InFilename)])
-    // ToDo
+    aFilename:=SetDirSeparators(InFilename);
+    Result:=ResolveDots(aFilename);
+    if FilenameIsAbsolute(Result) then
+    begin
+      if SearchLowUpCase(Result) then exit;
+    end else
+    begin
+      Result:=ResolveDots(BaseDirectory+Result);
+      if SearchLowUpCase(Result) then exit;
+    end;
+    exit('');
   end;
 
   // first search in foreign unit paths
@@ -1973,5 +1988,27 @@ begin
   end;
 end;
 
+procedure TPas2jsFilesCache.SaveToFile(ms: TMemoryStream; Filename: string);
+var
+  s: string;
+  l: Int64;
+begin
+  if Assigned(OnWriteFile) then
+  begin
+    l:=ms.Size-ms.Position;
+    if l>0 then
+    begin
+      SetLength(s,l);
+      ms.Read(s[1],l);
+    end
+    else
+      s:='';
+    OnWriteFile(Filename,s);
+  end else
+  begin
+    ms.SaveToFile(Filename);
+  end;
+end;
+
 end.
 

+ 34 - 3
packages/pastojs/src/pas2jslogger.pp

@@ -59,6 +59,12 @@ type
   TPas2jsLogger = class
   private
     FEncoding: string;
+    FLastMsgCol: integer;
+    FLastMsgFile: string;
+    FLastMsgLine: integer;
+    FLastMsgNumber: integer;
+    FLastMsgTxt: string;
+    FLastMsgType: TMessageType;
     FMsgNumberDisabled: PInteger;// sorted ascending
     FMsgNumberDisabledCount: integer;
     FMsg: TFPList; // list of TPas2jsMessage
@@ -107,6 +113,7 @@ type
     procedure Flush;
     procedure CloseOutputFile;
     procedure Reset;
+    procedure ClearLastMsg;
   public
     property Encoding: string read FEncoding write SetEncoding; // normalized
     property MsgCount: integer read GetMsgCount;
@@ -117,7 +124,13 @@ type
     property ShowMsgNumbers: boolean read FShowMsgNumbers write FShowMsgNumbers;
     property ShowMsgTypes: TMessageTypes read FShowMsgTypes write FShowMsgTypes;
     property Sorted: boolean read FSorted write SetSorted;
-    Property OnLog : TPas2jsLogEvent Read FOnLog Write FonLog;
+    property OnLog: TPas2jsLogEvent read FOnLog write FOnLog;
+    property LastMsgType: TMessageType read FLastMsgType write FLastMsgType;
+    property LastMsgFile: string read FLastMsgFile write FLastMsgFile;
+    property LastMsgLine: integer read FLastMsgLine write FLastMsgLine;
+    property LastMsgCol: integer read FLastMsgCol write FLastMsgCol;
+    property LastMsgTxt: string read FLastMsgTxt write FLastMsgTxt;
+    property LastMsgNumber: integer read FLastMsgNumber write FLastMsgNumber;
   end;
 
 function CompareP2JMessage(Item1, Item2: Pointer): Integer;
@@ -662,6 +675,7 @@ end;
 
 procedure TPas2jsLogger.LogRaw(const Msg: string);
 begin
+  ClearLastMsg;
   DoLogRaw(Msg,False);
 end;
 
@@ -679,7 +693,8 @@ procedure TPas2jsLogger.LogPlain(const Msg: string);
 var
   s: String;
 begin
-  if encoding='json' then
+  ClearLastMsg;
+  if Encoding='json' then
     begin
     s:=FormatJSONMsg(mtInfo,Msg,0,'',0,0);
     DoLogRaw(s,True);
@@ -714,7 +729,13 @@ begin
     s:=FormatJSONMsg(MsgType,Msg,MsgNumber,Filename,Line,Col)
   else
     s:=FormatMsg(MsgType,Msg,MsgNumber,Filename,Line,Col);
-  LogRaw(s);
+  FLastMsgType:=MsgType;
+  FLastMsgNumber:=MsgNumber;
+  FLastMsgTxt:=Msg;
+  FLastMsgFile:=Filename;
+  FLastMsgLine:=Line;
+  FLastMsgCol:=Col;
+  DoLogRaw(s,False);
 end;
 
 procedure TPas2jsLogger.LogMsgIgnoreFilter(MsgNumber: integer;
@@ -830,5 +851,15 @@ begin
   FShowMsgTypes:=DefaultLogMsgTypes;
 end;
 
+procedure TPas2jsLogger.ClearLastMsg;
+begin
+  FLastMsgType:=mtInfo;
+  FLastMsgNumber:=0;
+  FLastMsgTxt:='';
+  FLastMsgFile:='';
+  FLastMsgLine:=0;
+  FLastMsgCol:=0;
+end;
+
 end.
 

+ 13 - 2
packages/pastojs/src/pas2jspparser.pp

@@ -48,7 +48,8 @@ type
     property Log: TPas2jsLogger read FLog write FLog;
   end;
 
-  TOnFindModule = function(const aUnitname: String): TPasModule of object;
+  TOnFindModule = function(const AUnitName, InFilename: String; NameExpr,
+      InFileExpr: TPasExpr): TPasModule of object;
   TOnCheckSrcName = procedure(const aElement: TPasElement) of object;
 
   { TPas2jsCompilerResolver }
@@ -66,6 +67,8 @@ type
       const ASrcPos: TPasSourcePos): TPasElement;
       overload; override;
     function FindModule(const aUnitname: String): TPasModule; override;
+    function FindUnit(const AName, InFilename: String; NameExpr,
+      InFileExpr: TPasExpr): TPasModule; override;
     procedure ContinueParsing; override;
   public
     Owner: TObject;
@@ -160,7 +163,15 @@ end;
 
 function TPas2jsCompilerResolver.FindModule(const aUnitname: String): TPasModule;
 begin
-  Result:=OnFindModule(aUnitname);
+  raise EPasResolve.Create('Call TPas2jsCompilerResolver.FindModule(name,expr,...) instead');
+  Result:=nil;
+  if aUnitname='' then ;
+end;
+
+function TPas2jsCompilerResolver.FindUnit(const AName, InFilename: String;
+  NameExpr, InFileExpr: TPasExpr): TPasModule;
+begin
+  Result:=OnFindModule(AName,InFilename,NameExpr,InFileExpr);
 end;
 
 procedure TPas2jsCompilerResolver.ContinueParsing;

+ 7 - 2
packages/pastojs/tests/tcmodules.pas

@@ -56,7 +56,8 @@ type
     procedure SetModule(AValue: TPasModule);
   public
     destructor Destroy; override;
-    function FindModule(const AName: String): TPasModule; override;
+    function FindUnit(const AName, InFilename: String; NameExpr,
+      InFileExpr: TPasExpr): TPasModule; override;
     property OnFindUnit: TOnFindUnit read FOnFindUnit write FOnFindUnit;
     property Filename: string read FFilename write FFilename;
     property Resolver: TStreamResolver read FResolver write FResolver;
@@ -659,11 +660,15 @@ begin
   inherited Destroy;
 end;
 
-function TTestEnginePasResolver.FindModule(const AName: String): TPasModule;
+function TTestEnginePasResolver.FindUnit(const AName, InFilename: String;
+  NameExpr, InFileExpr: TPasExpr): TPasModule;
 begin
   Result:=nil;
+  if InFilename<>'' then
+    RaiseNotYetImplemented(20180224101926,InFileExpr,'Use testcase tcunitsearch instead');
   if Assigned(OnFindUnit) then
     Result:=OnFindUnit(AName);
+  if NameExpr=nil then ;
 end;
 
 { TCustomTestModule }

+ 586 - 0
packages/pastojs/tests/tcunitsearch.pas

@@ -0,0 +1,586 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2018 by Michael Van Canneyt
+
+    Unit tests for Pascal-to-Javascript converter class.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************
+
+ Examples:
+    ./testpas2js --suite=TTestModule.TestEmptyProgram
+    ./testpas2js --suite=TTestModule.TestEmptyUnit
+}
+unit tcunitsearch;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, contnrs,
+  fpcunit, testregistry,
+  Pas2jsFileUtils, Pas2jsCompiler, Pas2jsFileCache, Pas2jsLogger,
+  tcmodules, PScanner;
+
+type
+
+  { TTestCompiler }
+
+  TTestCompiler = class(TPas2jsCompiler)
+  private
+    FExitCode: longint;
+  protected
+    function GetExitCode: Longint; override;
+    procedure SetExitCode(Value: Longint); override;
+  end;
+
+  { TCLIFile }
+
+  TCLIFile = class
+  public
+    Filename: string;
+    Source: string;
+    Age: TPas2jsFileAgeTime;
+    Attr: TPas2jsFileAttr;
+    constructor Create(const aFilename, Src: string; aAge: TPas2jsFileAgeTime;
+       aAttr: TPas2jsFileAttr);
+  end;
+
+  { TCLILogMsg }
+
+  TCLILogMsg = class
+  public
+    Msg: string;
+    MsgTxt: string;
+    MsgType: TMessageType;
+    MsgNumber: integer;
+    MsgFile: string;
+    MsgLine: integer;
+    MsgCol: integer;
+  end;
+
+  { TCustomTestCLI }
+
+  TCustomTestCLI = class(TTestCase)
+  private
+    FErrorCol: integer;
+    FErrorFile: string;
+    FErrorLine: integer;
+    FErrorMsg: string;
+    FErrorNumber: integer;
+    FWorkDir: string;
+    FCompilerExe: string;
+    FCompiler: TTestCompiler;
+    FDefaultFileAge: longint;
+    FFiles: TObjectList; // list of TCLIFile
+    FLogMsgs: TObjectList; // list ot TCLILogMsg
+    FParams: TStringList;
+    function GetExitCode: integer;
+    function GetFiles(Index: integer): TCLIFile;
+    function GetLogMsgs(Index: integer): TCLILogMsg;
+    procedure SetExitCode(const AValue: integer);
+    procedure SetWorkDir(const AValue: string);
+  protected
+    procedure SetUp; override;
+    procedure TearDown; override;
+    procedure DoLog(Sender: TObject; const Msg: String);
+    Function OnReadDirectory(Dir: TPas2jsCachedDirectory): boolean; virtual;
+    Function OnReadFile(aFilename: string; var aSource: string): boolean; virtual;
+    procedure OnWriteFile(aFilename: string; Source: string);
+    procedure WriteSources;
+  public
+    constructor Create; override;
+    destructor Destroy; override;
+    procedure Compile(const Args: array of string; ExpectedExitCode: longint = 0);
+    property Compiler: TTestCompiler read FCompiler;
+    property CompilerExe: string read FCompilerExe write FCompilerExe;
+    property Params: TStringList read FParams;
+    property Files[Index: integer]: TCLIFile read GetFiles; // files an directories
+    function FileCount: integer;
+    function FindFile(Filename: string): TCLIFile; // files and directories
+    function ExpandFilename(const Filename: string): string;
+    function AddFile(Filename, Source: string): TCLIFile;
+    function AddFile(Filename: string; const SourceLines: array of string): TCLIFile;
+    function AddUnit(Filename: string; const Intf, Impl: array of string): TCLIFile;
+    function AddDir(Filename: string): TCLIFile;
+    property WorkDir: string read FWorkDir write SetWorkDir;
+    property DefaultFileAge: longint read FDefaultFileAge write FDefaultFileAge;
+    property ExitCode: integer read GetExitCode write SetExitCode;
+    property LogMsgs[Index: integer]: TCLILogMsg read GetLogMsgs;
+    function GetLogCount: integer;
+    property ErrorMsg: string read FErrorMsg write FErrorMsg;
+    property ErrorFile: string read FErrorFile write FErrorFile;
+    property ErrorLine: integer read FErrorLine write FErrorLine;
+    property ErrorCol: integer read FErrorCol write FErrorCol;
+    property ErrorNumber: integer read FErrorNumber write FErrorNumber;
+  end;
+
+  { TTestCLI_UnitSearch }
+
+  TTestCLI_UnitSearch = class(TCustomTestCLI)
+  published
+    procedure TestUS_Program;
+
+    procedure TestUS_UsesInFile;
+    procedure TestUS_UsesInFile_Duplicate;
+    procedure TestUS_UsesInFile_IndirectDuplicate;
+  end;
+
+function LinesToStr(const Lines: array of string): string;
+
+implementation
+
+function LinesToStr(const Lines: array of string): string;
+var
+  i: Integer;
+begin
+  Result:='';
+  for i:=low(Lines) to high(Lines) do
+    Result:=Result+Lines[i]+LineEnding;
+end;
+
+{ TCLIFile }
+
+constructor TCLIFile.Create(const aFilename, Src: string;
+  aAge: TPas2jsFileAgeTime; aAttr: TPas2jsFileAttr);
+begin
+  Filename:=aFilename;
+  Source:=Src;
+  Age:=aAge;
+  Attr:=aAttr;
+end;
+
+{ TTestCompiler }
+
+function TTestCompiler.GetExitCode: Longint;
+begin
+  Result:=FExitCode;
+end;
+
+procedure TTestCompiler.SetExitCode(Value: Longint);
+begin
+  FExitCode:=Value;
+end;
+
+{ TCustomTestCLI }
+
+function TCustomTestCLI.GetFiles(Index: integer): TCLIFile;
+begin
+  Result:=TCLIFile(FFiles[Index]);
+end;
+
+function TCustomTestCLI.GetExitCode: integer;
+begin
+  Result:=Compiler.ExitCode;
+end;
+
+function TCustomTestCLI.GetLogMsgs(Index: integer): TCLILogMsg;
+begin
+  Result:=TCLILogMsg(FLogMsgs[Index]);
+end;
+
+procedure TCustomTestCLI.SetExitCode(const AValue: integer);
+begin
+  Compiler.ExitCode:=AValue;
+end;
+
+procedure TCustomTestCLI.SetWorkDir(const AValue: string);
+var
+  NewValue: String;
+begin
+  NewValue:=IncludeTrailingPathDelimiter(ResolveDots(AValue));
+  if FWorkDir=NewValue then Exit;
+  FWorkDir:=NewValue;
+end;
+
+procedure TCustomTestCLI.SetUp;
+begin
+  inherited SetUp;
+  FDefaultFileAge:=DateTimeToFileDate(Now);
+  {$IFDEF Windows}
+  BaseDir:='P:\pascal\test';
+  CompilerExe:='P:\pascal\bin\pas2js.exe';
+  {$ELSE}
+  WorkDir:='/home/user';
+  CompilerExe:='/usr/bin/pas2js';
+  {$ENDIF}
+  FCompiler:=TTestCompiler.Create;
+  Compiler.Log.OnLog:=@DoLog;
+  Compiler.FileCache.DirectoryCache.OnReadDirectory:=@OnReadDirectory;
+  Compiler.FileCache.OnReadFile:=@OnReadFile;
+  Compiler.FileCache.OnWriteFile:=@OnWriteFile;
+end;
+
+procedure TCustomTestCLI.TearDown;
+begin
+  FreeAndNil(FCompiler);
+  FParams.Clear;
+  FFiles.Clear;
+  FLogMsgs.Clear;
+  FErrorMsg:='';
+  FErrorFile:='';
+  FErrorLine:=0;
+  FErrorCol:=0;
+  FErrorNumber:=0;
+  inherited TearDown;
+end;
+
+procedure TCustomTestCLI.DoLog(Sender: TObject; const Msg: String);
+var
+  LogMsg: TCLILogMsg;
+begin
+  {$IFDEF VerbosePasResolver}
+  writeln('TCustomTestCLI.DoLog ',Msg,' File=',Compiler.Log.LastMsgFile,' Line=',Compiler.Log.LastMsgLine);
+  {$ENDIF}
+  LogMsg:=TCLILogMsg.Create;
+  LogMsg.Msg:=Msg;
+  LogMsg.MsgTxt:=Compiler.Log.LastMsgTxt;
+  LogMsg.MsgType:=Compiler.Log.LastMsgType;
+  LogMsg.MsgFile:=Compiler.Log.LastMsgFile;
+  LogMsg.MsgLine:=Compiler.Log.LastMsgLine;
+  LogMsg.MsgCol:=Compiler.Log.LastMsgCol;
+  LogMsg.MsgNumber:=Compiler.Log.LastMsgNumber;
+  FLogMsgs.Add(LogMsg);
+  if (LogMsg.MsgType<=mtError) then
+  begin
+    if (ErrorFile='')
+        or ((ErrorLine<1) and (LogMsg.MsgLine>0)) then
+    begin
+      ErrorMsg:=LogMsg.MsgTxt;
+      ErrorFile:=LogMsg.MsgFile;
+      ErrorLine:=LogMsg.MsgLine;
+      ErrorCol:=LogMsg.MsgCol;
+    end;
+  end;
+end;
+
+function TCustomTestCLI.OnReadDirectory(Dir: TPas2jsCachedDirectory): boolean;
+var
+  i: Integer;
+  aFile: TCLIFile;
+  Path: String;
+begin
+  Path:=Dir.Path;
+  //writeln('TCustomTestCLI.ReadDirectory START ',Path,' ',Dir.Count);
+  Dir.Add('.',DefaultFileAge,faDirectory,4096);
+  Dir.Add('..',DefaultFileAge,faDirectory,4096);
+  for i:=0 to FileCount-1 do
+    begin
+    aFile:=Files[i];
+    if CompareFilenames(ExtractFilePath(aFile.Filename),Path)<>0 then continue;
+    //writeln('TCustomTestCLI.ReadDirectory ',aFile.Filename);
+    Dir.Add(ExtractFileName(aFile.Filename),aFile.Age,aFile.Attr,length(aFile.Source));
+    end;
+  //writeln('TCustomTestCLI.ReadDirectory END ',Path,' ',Dir.Count);
+  Result:=true;
+end;
+
+function TCustomTestCLI.OnReadFile(aFilename: string; var aSource: string
+  ): boolean;
+var
+  aFile: TCLIFile;
+begin
+  aFile:=FindFile(aFilename);
+  //writeln('TCustomTestCLI.ReadFile ',aFilename,' Found=',aFile<>nil);
+  if aFile=nil then exit(false);
+  if (faDirectory and aFile.Attr)>0 then
+  begin
+    {$IFDEF VerbosePasResolver}
+    writeln('[20180224000557] TCustomTestCLI.OnReadFile ',aFilename);
+    {$ENDIF}
+    EPas2jsFileCache.Create('TCustomTestCLI.OnReadFile: reading directory '+aFilename);
+  end;
+  aSource:=aFile.Source;
+  Result:=true;
+end;
+
+procedure TCustomTestCLI.OnWriteFile(aFilename: string; Source: string);
+var
+  aFile: TCLIFile;
+begin
+  aFile:=FindFile(aFilename);
+  {$IFDEF VerbosePasResolver}
+  writeln('TCustomTestCLI.WriteFile ',aFilename,' Found=',aFile<>nil);
+  {$ENDIF}
+  if aFile<>nil then
+  begin
+    if faDirectory and aFile.Attr>0 then
+    begin
+      {$IFDEF VerbosePasResolver}
+      writeln('[20180223175616] TCustomTestCLI.OnWriteFile ',aFilename);
+      {$ENDIF}
+      raise EPas2jsFileCache.Create('unable to write file to directory "'+aFilename+'"');
+    end;
+  end else
+  begin
+    aFile:=TCLIFile.Create(aFilename,'',0,0);
+    FFiles.Add(aFile);
+  end;
+  aFile.Source:=Source;
+  aFile.Attr:=faDirectory;
+  aFile.Age:=DateTimeToFileDate(Now);
+end;
+
+procedure TCustomTestCLI.WriteSources;
+var
+  i, j, aRow, aCol: Integer;
+  aFile: TCLIFile;
+  SrcLines: TStringList;
+  Line, aFilename: String;
+  IsSrc: Boolean;
+begin
+  writeln('TCustomTestCLI.WriteSources START');
+  aFilename:=ErrorFile;
+  aRow:=ErrorLine;
+  aCol:=ErrorCol;
+  SrcLines:=TStringList.Create;
+  try
+    for i:=0 to FileCount-1 do
+    begin
+      aFile:=Files[i];
+      if (faDirectory and aFile.Attr)>0 then continue;
+      writeln('Testcode:-File="',aFile.Filename,'"----------------------------------:');
+      SrcLines.Text:=aFile.Source;
+      IsSrc:=ExtractFilename(aFile.Filename)=ExtractFileName(aFilename);
+      for j:=1 to SrcLines.Count do
+        begin
+        Line:=SrcLines[j-1];
+        if IsSrc and (j=aRow) then
+          begin
+          write('*');
+          Line:=LeftStr(Line,aCol-1)+'|'+copy(Line,aCol,length(Line));
+          end;
+        writeln(Format('%:4d: ',[j]),Line);
+        end;
+    end;
+  finally
+    SrcLines.Free;
+  end;
+end;
+
+constructor TCustomTestCLI.Create;
+begin
+  inherited Create;
+  FFiles:=TObjectList.Create(true);
+  FLogMsgs:=TObjectList.Create(true);
+  FParams:=TStringList.Create;
+end;
+
+destructor TCustomTestCLI.Destroy;
+begin
+  FreeAndNil(FFiles);
+  FreeAndNil(FLogMsgs);
+  FreeAndNil(FParams);
+  inherited Destroy;
+end;
+
+procedure TCustomTestCLI.Compile(const Args: array of string;
+  ExpectedExitCode: longint);
+var
+  i: Integer;
+begin
+  AssertEquals('Initial System.ExitCode',0,system.ExitCode);
+  for i:=low(Args) to High(Args) do
+    Params.Add(Args[i]);
+  try
+    try
+      //writeln('TCustomTestCLI.Compile WorkDir=',WorkDir);
+      Compiler.Run(CompilerExe,WorkDir,Params,false);
+    except
+      on E: ECompilerTerminate do
+      begin
+        {$IFDEF VerbosePasResolver}
+        writeln('TCustomTestCLI.Compile ',E.ClassName,':',E.Message);
+        {$ENDIF}
+      end;
+      on E: Exception do
+      begin
+        {$IFDEF VerbosePasResolver}
+        writeln('TCustomTestCLI.Compile ',E.ClassName,':',E.Message);
+        {$ENDIF}
+        Fail('TCustomTestCLI.Compile '+E.ClassName+':'+E.Message);
+      end;
+    end;
+  finally
+    Compiler.Log.CloseOutputFile;
+  end;
+
+  if ExpectedExitCode<>ExitCode then
+  begin
+    WriteSources;
+    AssertEquals('ExitCode',ExpectedExitCode,ExitCode);
+  end;
+end;
+
+function TCustomTestCLI.FileCount: integer;
+begin
+  Result:=FFiles.Count;
+end;
+
+function TCustomTestCLI.FindFile(Filename: string): TCLIFile;
+var
+  i: Integer;
+begin
+  Filename:=ExpandFilename(Filename);
+  for i:=0 to FileCount-1 do
+    if CompareFilenames(Files[i].Filename,Filename)=0 then
+      exit(Files[i]);
+  Result:=nil;
+end;
+
+function TCustomTestCLI.ExpandFilename(const Filename: string): string;
+begin
+  Result:=SetDirSeparators(Filename);
+  if not FilenameIsAbsolute(Result) then
+    Result:=WorkDir+Result;
+  Result:=ResolveDots(Result);
+end;
+
+function TCustomTestCLI.AddFile(Filename, Source: string): TCLIFile;
+begin
+  Filename:=ExpandFilename(Filename);
+  Result:=FindFile(Filename);
+  if Result<>nil then
+    raise Exception.Create('[20180224001050] TCustomTestCLI.AddFile already exists: '+Filename);
+  FFiles.Add(TCLIFile.Create(Filename,Source,DefaultFileAge,faNormal));
+  AddDir(ExtractFilePath(Filename));
+end;
+
+function TCustomTestCLI.AddFile(Filename: string;
+  const SourceLines: array of string): TCLIFile;
+begin
+  Result:=AddFile(Filename,LinesToStr(SourceLines));
+end;
+
+function TCustomTestCLI.AddUnit(Filename: string; const Intf,
+  Impl: array of string): TCLIFile;
+var
+  Name: String;
+begin
+  Name:=ExtractFilenameOnly(Filename);
+  Result:=AddFile(Filename,
+    'unit '+Name+';'+LineEnding
+    +'interface'+LineEnding
+    +LinesToStr(Intf)
+    +'implementation'+LineEnding
+    +LinesToStr(Impl)
+    +'end.'+LineEnding);
+end;
+
+function TCustomTestCLI.AddDir(Filename: string): TCLIFile;
+var
+  p: Integer;
+  Dir: String;
+  aFile: TCLIFile;
+begin
+  Result:=nil;
+  Filename:=ExpandFilename(Filename);
+  p:=length(Filename);
+  while p>1 do
+  begin
+    if Filename[p]=PathDelim then
+    begin
+      Dir:=LeftStr(Filename,p-1);
+      aFile:=FindFile(Dir);
+      if Result=nil then
+        Result:=aFile;
+      if aFile=nil then
+        FFiles.Add(TCLIFile.Create(Dir,'',DefaultFileAge,faDirectory))
+      else if (aFile.Attr and faDirectory)=0 then
+      begin
+        {$IFDEF VerbosePasResolver}
+        writeln('[20180224001036] TCustomTestCLI.AddDir Dir=',Dir);
+        {$ENDIF}
+        raise EPas2jsFileCache.Create('[20180224001036] TCustomTestCLI.AddDir Dir='+Dir);
+      end;
+      dec(p);
+    end else
+      dec(p);
+  end;
+end;
+
+function TCustomTestCLI.GetLogCount: integer;
+begin
+  Result:=FLogMsgs.Count;
+end;
+
+{ TTestCLI_UnitSearch }
+
+procedure TTestCLI_UnitSearch.TestUS_Program;
+begin
+  AddUnit('system.pp',[''],['']);
+  AddFile('test1.pas',[
+    'begin',
+    'end.']);
+  Compile(['test1.pas','-va']);
+end;
+
+procedure TTestCLI_UnitSearch.TestUS_UsesInFile;
+begin
+  AddUnit('system.pp',[''],['']);
+  AddUnit('unit1.pas',
+  ['uses bird in ''unit2.pas'';',
+   'var a: longint;'],
+  ['']);
+  AddUnit('unit2.pas',
+  ['var b: longint;'],
+  ['']);
+  AddFile('test1.pas',[
+    'uses foo in ''unit1.pas'', bar in ''unit2.pas'';',
+    'begin',
+    '  bar.b:=foo.a;',
+    '  a:=b;',
+    'end.']);
+  Compile(['test1.pas','-Jc']);
+end;
+
+procedure TTestCLI_UnitSearch.TestUS_UsesInFile_Duplicate;
+begin
+  AddUnit('system.pp',[''],['']);
+  AddUnit('unit1.pas',
+  ['var a: longint;'],
+  ['']);
+  AddUnit('sub/unit1.pas',
+  ['var b: longint;'],
+  ['']);
+  AddFile('test1.pas',[
+    'uses foo in ''unit1.pas'', bar in ''sub/unit1.pas'';',
+    'begin',
+    '  bar.b:=foo.a;',
+    '  a:=b;',
+    'end.']);
+  Compile(['test1.pas','-Jc'],ExitCodeSyntaxError);
+  AssertEquals('ErrorMsg','Duplicate file found: "/home/user/sub/unit1.pas" and "/home/user/unit1.pas"',ErrorMsg);
+end;
+
+procedure TTestCLI_UnitSearch.TestUS_UsesInFile_IndirectDuplicate;
+begin
+  AddUnit('system.pp',[''],['']);
+  AddUnit('unit1.pas',
+  ['var a: longint;'],
+  ['']);
+  AddUnit('sub/unit1.pas',
+  ['var b: longint;'],
+  ['']);
+  AddUnit('unit2.pas',
+  ['uses unit1 in ''unit1.pas'';'],
+  ['']);
+  AddFile('test1.pas',[
+    'uses unit2, foo in ''sub/unit1.pas'';',
+    'begin',
+    'end.']);
+  Compile(['test1.pas','-Jc'],ExitCodeSyntaxError);
+  AssertEquals('ErrorMsg','Duplicate file found: "/home/user/sub/unit1.pas" and "/home/user/unit1.pas"',ErrorMsg);
+end;
+
+Initialization
+  RegisterTests([TTestCLI_UnitSearch]);
+end.
+

+ 5 - 1
packages/pastojs/tests/testpas2js.lpi

@@ -32,7 +32,7 @@
         <PackageName Value="FCL"/>
       </Item2>
     </RequiredPackages>
-    <Units Count="9">
+    <Units Count="10">
       <Unit0>
         <Filename Value="testpas2js.pp"/>
         <IsPartOfProject Value="True"/>
@@ -72,6 +72,10 @@
         <IsPartOfProject Value="True"/>
         <UnitName Value="Pas2JsFiler"/>
       </Unit8>
+      <Unit9>
+        <Filename Value="tcunitsearch.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit9>
     </Units>
   </ProjectOptions>
   <CompilerOptions>

+ 1 - 1
packages/pastojs/tests/testpas2js.pp

@@ -18,7 +18,7 @@ program testpas2js;
 
 uses
   Classes, consoletestrunner, tcconverter, tcmodules, tcoptimizations, tcsrcmap,
-  tcfiler, Pas2JsFiler;
+  tcfiler, Pas2JsFiler, tcunitsearch;
 
 type