| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package parser |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "mojom/mojom_tool/mojom" |
| "os" |
| "path/filepath" |
| ) |
| |
| /////////////////////////////////////////////////////////////////////// |
| /// Type ParseDriver |
| ////////////////////////////////////////////////////////////////////// |
| |
| // A ParseDriver is used to parse a list of .mojom files. |
| // |
| // Construct a new ParseDriver via NewDriver() and then call ParseFiles() |
| // passing in the list of top-level .mojom files to be parsed. Any imported |
| // files will also be parsed. |
| // |
| // We attempt to find the file named by a given path, both top-level and |
| // imported, using the following algorithm: |
| // (1) If the specified path is an absolute path we use that path |
| // (2) Otherwise if the file was imported from another file we first attempt |
| // to find a file with the specified path relative to the directory of the |
| // importing file |
| // (3) Otherwise if the file was imported we attempt to find a file with the |
| // specified path relative to one of the specified import directories. |
| // (4) Otherwise we attempt to find a file with the specified path relative to |
| // the current working directory. |
| // |
| // After all files have been parsed the populated |MojomDescriptor| will be |
| // resovled, and then Mojo serialization data will be computed for use by |
| // the code generators. |
| // |
| // A poulated |MojomDescriptor| is returned. |
| // If there was an error then the returned |err| will be non-nil. |
| // |
| // A ParseDriver may only be used once. |
| type ParseDriver struct { |
| fileProvider FileProvider |
| fileExtractor FileExtractor |
| parseInvoker ParseInvoker |
| importDirs []string |
| debugMode bool |
| metaDataOnlyMode bool |
| } |
| |
| // NewDriver consructs a new ParseDriver. |
| // |
| // importDirectories is a list of paths, either absolute or relative to the |
| // current working directory, of directories in which we should search for |
| // imported .mojom files. |
| // |
| // If debugMode is true we print to standard out the parse tree resulting |
| // from each file parsing. In non-debug mode the parsers do not explicitly |
| // construct a parse tree. |
| // |
| // If metaDataOnlyMode is true then: |
| // (1) The parsers will only parse the mojom file metadata (module statement, |
| // import statements, file-level attributes). The mojom declarations will not |
| // be parsed. The result will be as if the .mojom file did not have any mojom |
| // declarations. The resolution and validation phases will not be executed. |
| // (2) References to imported files will not be followed and imported files |
| // will not be parsed. Only explicitly specified files will be parsed. |
| // This implies that the |MojomFile|s will contain partial information about |
| // their imports. Each |ImportedFile| will contain its |SpecifiedName| but not |
| // its |CanonicalFileName|. |
| func NewDriver(importDirectories []string, debugMode, metaDataOnlyMode bool) *ParseDriver { |
| fileProvider := new(OSFileProvider) |
| fileProvider.importDirs = importDirectories |
| return newDriver(importDirectories, debugMode, metaDataOnlyMode, fileProvider, |
| DefaultFileExtractor(0), DefaultParseInvoker(0)) |
| } |
| |
| // This version of the factory is used in tests. |
| func newDriver(importDirectories []string, debugMode, metaDataOnlyMode bool, fileProvider FileProvider, |
| fileExtractor FileExtractor, parseInvoker ParseInvoker) *ParseDriver { |
| p := ParseDriver{fileProvider: fileProvider, fileExtractor: fileExtractor, parseInvoker: parseInvoker, |
| importDirs: importDirectories, debugMode: debugMode, metaDataOnlyMode: metaDataOnlyMode} |
| return &p |
| |
| } |
| |
| // Parses each of the given .mojom files and all of the files in the |
| // import graph rooted by each file. A single MojomDescriptor is created and |
| // populated with the result of parsing all of those files. If the parsing is |
| // successful then err will be nil. |
| // |
| // fileNames must not be nil or we will panic. |
| func (d *ParseDriver) ParseFiles(fileNames []string) (descriptor *mojom.MojomDescriptor, err error) { |
| if fileNames == nil { |
| // We panic instead of returning an error here because this would be a programming error |
| // as opposed to an error in the input. |
| panic("fileNames may not be nil.") |
| } |
| filesToProcess := make([]*FileReference, len(fileNames)) |
| descriptor = mojom.NewMojomDescriptor() |
| for i, fileName := range fileNames { |
| filesToProcess[i] = &FileReference{specifiedPath: fileName} |
| } |
| |
| for len(filesToProcess) > 0 { |
| currentFile := filesToProcess[0] |
| filesToProcess = filesToProcess[1:] |
| if err = d.fileProvider.findFile(currentFile); err != nil { |
| return |
| } |
| |
| var importedFrom *mojom.MojomFile = nil |
| if currentFile.importedFrom != nil { |
| importedFrom = currentFile.importedFrom.mojomFile |
| // Tell the importing file about the absolute path of the imported file. |
| // Note that we must do this even if the imported file has already been processed |
| // because a given file may be imported by multiple files and each of those need |
| // to be told about the absolute path of the imported file. |
| importedFrom.SetCanonicalImportName(currentFile.specifiedPath, currentFile.absolutePath) |
| } |
| |
| if !descriptor.ContainsFile(currentFile.absolutePath) { |
| contents, fileReadError := d.fileProvider.provideContents(currentFile) |
| if fileReadError != nil { |
| err = fileReadError |
| return |
| } |
| // topLevelFileName should be non-empty if and only if the current file is a top-level file. |
| topLevelFileName := "" |
| if importedFrom == nil { |
| topLevelFileName = currentFile.specifiedPath |
| } |
| parser := MakeParser(currentFile.absolutePath, topLevelFileName, |
| contents, descriptor, importedFrom) |
| parser.SetDebugMode(d.debugMode) |
| parser.SetMetaDataOnlyMode(d.metaDataOnlyMode) |
| // Invoke parser.Parse() (but skip doing so in tests sometimes.) |
| d.parseInvoker.invokeParse(&parser) |
| |
| if d.debugMode { |
| fmt.Printf("\nParseTree for %s:", currentFile) |
| fmt.Println(parser.GetParseTree()) |
| } |
| |
| if !parser.OK() { |
| err = parser.GetError() |
| return |
| } |
| currentFile.mojomFile = d.fileExtractor.extractMojomFile(&parser) |
| if !d.metaDataOnlyMode { |
| // In meta-data-only mode we do not follow import statements. |
| for _, importedFile := range currentFile.mojomFile.Imports { |
| // Note that it is important that we append all of the imported files here even |
| // if some of them have already been processed. That is because when the imported |
| // file is pulled from the queue it will be pre-processed during which time the |
| // absolute path to the file will be discovered and this absolute path will be |
| // set in |mojomFile| which is necessary for serializing mojomFile. |
| filesToProcess = append(filesToProcess, |
| &FileReference{importedFrom: currentFile, specifiedPath: importedFile.SpecifiedName}) |
| } |
| } |
| } |
| } |
| |
| // Perform type and value resolution |
| if err = descriptor.Resolve(); err != nil { |
| return |
| } |
| |
| // Compute data for generators. |
| if err = descriptor.ComputeFinalData(); err != nil { |
| return |
| } |
| |
| // Check for ill-founded types. |
| err = descriptor.DetectIllFoundedTypes() |
| return |
| } |
| |
| type FileReference struct { |
| mojomFile *mojom.MojomFile |
| importedFrom *FileReference |
| specifiedPath string |
| absolutePath string |
| directoryPath string |
| } |
| |
| func (f FileReference) String() string { |
| if f.importedFrom != nil { |
| return fmt.Sprintf("%s imported from file %s.", |
| f.specifiedPath, f.importedFrom.specifiedPath) |
| } else { |
| return fmt.Sprintf("%s", f.specifiedPath) |
| } |
| } |
| |
| // FileExtractor is an abstraction that allows us to inject fake MojomFiles |
| // in tests. |
| type FileExtractor interface { |
| extractMojomFile(parser *Parser) *mojom.MojomFile |
| } |
| |
| type DefaultFileExtractor int |
| |
| func (DefaultFileExtractor) extractMojomFile(parser *Parser) *mojom.MojomFile { |
| return parser.GetMojomFile() |
| } |
| |
| // ParseInvoker is an abstraction that allows us to skip actually invoking |
| // the parse method in tests of the driver. |
| type ParseInvoker interface { |
| invokeParse(parser *Parser) |
| } |
| |
| type DefaultParseInvoker int |
| |
| func (DefaultParseInvoker) invokeParse(parser *Parser) { |
| parser.Parse() |
| } |
| |
| // FileProvider is an abstraction that allows us to mock out the file system |
| // in tests. |
| type FileProvider interface { |
| provideContents(fileRef *FileReference) (contents string, fileReadError error) |
| |
| // findFile attempts to locate the file specified by the |specifiedPath| |
| // field of |fileRef|, taking into consideration also the |importedFrom| |
| // field. If the file can be located then the |absolutePath| and |
| // |directoryPath| path fields will be set and nil is returned. |
| // Otherwise a non-nil error is returned. |
| findFile(fileRef *FileReference) error |
| } |
| |
| type OSFileProvider struct { |
| importDirs []string |
| } |
| |
| func (p OSFileProvider) provideContents(fileRef *FileReference) (contents string, fileReadError error) { |
| data, err := ioutil.ReadFile(fileRef.absolutePath) |
| if err != nil { |
| fileReadError = fmt.Errorf("\nError while reading %s: %s\n\n", fileRef, err) |
| } else { |
| contents = string(data) |
| } |
| return |
| } |
| |
| // findFile populates the |absolutePath| and |directoryPath| fields of |
| // *fileRef. It attempts to find a file on the file system named by the |specifiedPath| |
| // field using the search algorithm described at the top of this file. |
| func (p *OSFileProvider) findFile(fileRef *FileReference) (err error) { |
| // If this FileReference has already been processed there is nothing to do. |
| if len(fileRef.absolutePath) > 0 { |
| return |
| } |
| |
| // If the specified path is already absolute we use that path. |
| if filepath.IsAbs(fileRef.specifiedPath) { |
| fileRef.absolutePath = fileRef.specifiedPath |
| fileRef.directoryPath = filepath.Dir(fileRef.absolutePath) |
| return |
| } |
| |
| // If the file was imported from another file... |
| if fileRef.importedFrom != nil { |
| // First attempt to find the file relative to the directory of the |
| // importing file. |
| attemptedName := filepath.Join(fileRef.importedFrom.directoryPath, fileRef.specifiedPath) |
| if isFile(attemptedName) { |
| fileRef.absolutePath, err = filepath.Abs(attemptedName) |
| fileRef.directoryPath = filepath.Dir(fileRef.absolutePath) |
| return |
| } |
| |
| // then search in the specified import directories. |
| if p.importDirs != nil { |
| for _, dir := range p.importDirs { |
| attemptedName := filepath.Join(dir, fileRef.specifiedPath) |
| if isFile(attemptedName) { |
| fileRef.absolutePath, err = filepath.Abs(attemptedName) |
| fileRef.directoryPath = filepath.Dir(fileRef.absolutePath) |
| return |
| } |
| } |
| |
| } |
| } |
| |
| // Finally look in the current working directory. |
| if isFile(fileRef.specifiedPath) { |
| if fileRef.absolutePath, err = filepath.Abs(fileRef.specifiedPath); err != nil { |
| return err |
| } |
| fileRef.directoryPath = filepath.Dir(fileRef.absolutePath) |
| return |
| } |
| |
| return fmt.Errorf("File not found: %s", fileRef) |
| } |
| |
| func isFile(path string) bool { |
| info, err := os.Stat(path) |
| if err != nil { |
| return false |
| } |
| return !info.IsDir() |
| } |